(id: string)
| 144 | private readonly eventBus: EventBus, |
| 145 | teamReconciler?: TeamReconcilerService, |
| 146 | runtimeRegistry?: RuntimeRegistry, |
| 147 | ) { |
| 148 | this.teamReconciler = teamReconciler ?? new TeamReconcilerService({ |
| 149 | eventBus, |
| 150 | sessionMessenger: this, |
| 151 | // 续催/唤醒统一由 MemberHeartbeatScheduler 轮询驱动;这里关闭内部 setTimeout 避免双驱动重复触发。 |
| 152 | // session 退出时的首次 reconcile(COMPLETED 判定 / 首次补催)仍即时执行,不依赖该定时器。 |
| 153 | scheduleReminders: false, |
| 154 | }); |
| 155 | this.runtimeCoordinator = new RuntimeCoordinator( |
| 156 | runtimeRegistry ?? new StaticRuntimeRegistry([ |
| 157 | new CliRuntimeDriver(), |
| 158 | new AcpRuntimeDriver(), |
| 159 | ]), |
| 160 | { |
| 161 | onTurnEvent: (event) => this.handleRuntimeTurnEvent(event), |
| 162 | onRuntimeState: (state) => this.handleRuntimeState(state), |
| 163 | onProcessEvent: (event) => this.handleRuntimeProcessEvent(event), |
| 164 | onDriverSessionDisposed: (sessionId) => revokeAgentApiCredential(sessionId), |
| 165 | }, |
| 166 | ); |
| 167 | |
| 168 | // Patches only mark the snapshot dirty. A low-frequency checkpoint keeps the |
| 169 | // hot stream away from SQLite while terminal paths still force a final flush. |
| 170 | this.eventBus.on('session:patch', ({ sessionId, patch }) => { |
| 171 | if (DEBUG_SNAPSHOT) { |
| 172 | const ops = (patch as Array<{ op?: string; path?: string }>).slice(0, 3) |
| 173 | .map((p) => `${p.op ?? '?'}:${p.path ?? '?'}`) |
| 174 | .join(', '); |
| 175 | console.log( |
| 176 | `[SessionManager:snapshot] patch sessionId=${sessionId} ops=${(patch as unknown[]).length} [${ops}]` |
| 177 | ); |
| 178 | } |
| 179 | this.scheduleSnapshotPersist(sessionId); |
| 180 | // 仅 agent 侧真实进展用作 TeamRun 成员心跳信号(节流落库);本地 user_message(含唤醒)被过滤。 |
| 181 | this.maybeRecordTeamRunHeartbeat(sessionId, patch); |
| 182 | }); |
| 183 | |
| 184 | this.eventBus.on('session:turn-completed', ({ sessionId }) => { |
| 185 | if (this.terminalSessions.has(sessionId)) return; |
| 186 | // The parser has already written raw stdout, the final assistant entry, |
| 187 | // usage and all other state from the turn.completed chunk at this point. |
| 188 | this.terminalSessions.set(sessionId, SessionStatus.COMPLETED); |
| 189 | this.startSessionFinalization(sessionId, 0, { logicalCompletion: true }); |
| 190 | }); |
| 191 | |
| 192 | this.eventBus.on('session:turn-failed', ({ sessionId }) => { |
| 193 | if (this.terminalSessions.has(sessionId)) return; |
| 194 | // A turn failure is terminal even when the CLI wrapper later exits 0 or |
| 195 | // without an exit code. Use a synthetic non-zero code for the shared |
| 196 | // finalization path so success-only post-processing cannot run. |
| 197 | this.terminalSessions.set(sessionId, SessionStatus.FAILED); |
| 198 | this.startSessionFinalization(sessionId, 1, { logicalCompletion: true }); |
| 199 | }); |
| 200 | |
| 201 | // NOTE: checkTaskAutoRevert is called directly (awaited) inside start() |
| 202 | // and sendMessage() to guarantee the task status is updated before the |
| 203 | // HTTP response is sent. A fire-and-forget EventBus listener here caused |
no test coverage detected