specrails-desktop 2.61.0 → 2.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/specrails-desktop.js +39 -5
- package/client/dist/assets/ActivityFeedPage-Bn2MpG5M.js +1 -0
- package/client/dist/assets/{AgentBrowserCapture-CkNWDZT3.js → AgentBrowserCapture-CHlXWdXq.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-yj-YCgrK.js +2 -0
- package/client/dist/assets/AgentModeCodePane-Xczcyghb.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-U1J6PaBo.js → AgentModeJobsPane-DtOW01ln.js} +1 -1
- package/client/dist/assets/AgentsPage-BMfJ4mzU.js +118 -0
- package/client/dist/assets/AnalyticsPage-CGn5cgtc.js +1 -0
- package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
- package/client/dist/assets/CodePage-C8NIginr.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-C87r4y26.js +1 -0
- package/client/dist/assets/{DocsDialog-CSlMPemn.js → DocsDialog-Z50a_KPv.js} +1 -1
- package/client/dist/assets/{DocsPage-BKlYAvWq.js → DocsPage-DIArbEPl.js} +1 -1
- package/client/dist/assets/{ExportDropdown-CweP_1GP.js → ExportDropdown-CJdCqYd7.js} +1 -1
- package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-BxLh4me-.js +19 -0
- package/client/dist/assets/{JobDetailModal-DYyGq1Vp.js → JobDetailModal-C7dWoRu6.js} +1 -1
- package/client/dist/assets/JobDetailPage-Ilu294WS.js +1 -0
- package/client/dist/assets/JobsPage-fPq1hefb.js +1 -0
- package/client/dist/assets/LoopBuilderPage-5M7IypEP.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-DkLRfsYX.js → LoopPreviewModal-ChI3NQC7.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-CuACkXFV.js → LoopWindowSurface-D1WB9i-9.js} +1 -1
- package/client/dist/assets/LoopsPage-CCwc0Biu.js +2 -0
- package/client/dist/assets/MinimizedChatsContext-eH_KaAZ1.js +1 -0
- package/client/dist/assets/PluginsPage-DfDfEfbW.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-9NHnG8JQ.js +1 -0
- package/client/dist/assets/RepositoryDeliveries-DgzNnJmn.js +5 -0
- package/client/dist/assets/{RepositoryScopeSelector-CeCr6Jz-.js → RepositoryScopeSelector-BSgUqk_u.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-D34bGpSA.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-B8yd1AGU.js → TemplatePreviewModal-CcfSxWQ7.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-BoafEcm4.js +16 -0
- package/client/dist/assets/Trans-CVSryAL1.js +1 -0
- package/client/dist/assets/agent-5bSixVzz.js +1 -0
- package/client/dist/assets/agent-BR4EHfhq.js +1 -0
- package/client/dist/assets/agent-C0IKkJP8.js +1 -0
- package/client/dist/assets/agent-C7li9ayC.js +1 -0
- package/client/dist/assets/agent-D0dRmEen.js +1 -0
- package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
- package/client/dist/assets/agent-Dip4h2YE.js +1 -0
- package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
- package/client/dist/assets/ban-wqfdzoXt.js +1 -0
- package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
- package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
- package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
- package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
- package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
- package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
- package/client/dist/assets/download-C9wP_42h.js +1 -0
- package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
- package/client/dist/assets/file-text-C0blZHLh.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-DZ3VWsHq.js → formatDistanceToNow-NVM6rK3i.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-9JiGlFrx.js → getTimezoneOffsetInMilliseconds-ZHg7WpdH.js} +1 -1
- package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
- package/client/dist/assets/index-Culan6y7.js +83 -0
- package/client/dist/assets/index-D1v33giW.css +2 -0
- package/client/dist/assets/jira-api-Bl8xEIpk.js +1 -0
- package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
- package/client/dist/assets/layers-BCLTgHXO.js +1 -0
- package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
- package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
- package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
- package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
- package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
- package/client/dist/assets/plus-fHDdjBk1.js +1 -0
- package/client/dist/assets/{project-repositories-CadLZOnr.js → project-repositories-CvJf1a6H.js} +1 -1
- package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
- package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
- package/client/dist/assets/save-LmPSfsvZ.js +1 -0
- package/client/dist/assets/search-BDVp3fkH.js +1 -0
- package/client/dist/assets/{settings-Dn-pXukr.js → settings-BO2N-oPf.js} +1 -1
- package/client/dist/assets/{settings-ByVE7jdZ.js → settings-BQChAs_w.js} +1 -1
- package/client/dist/assets/{settings-BBj_kZaD.js → settings-BzmYlcQB.js} +1 -1
- package/client/dist/assets/{settings-COlH_P33.js → settings-CGjXb6TX.js} +1 -1
- package/client/dist/assets/{settings-D77mC_Lb.js → settings-CMxEWY46.js} +1 -1
- package/client/dist/assets/{settings-DZKy7MqN.js → settings-C_dRSgI2.js} +1 -1
- package/client/dist/assets/{settings-B5Hy_Kol.js → settings-Hw7av-B1.js} +1 -1
- package/client/dist/assets/settings-nryN6wuS.js +1 -0
- package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
- package/client/dist/assets/spending-BfjSJjBd.js +1 -0
- package/client/dist/assets/square-D0JTbRSR.js +1 -0
- package/client/dist/assets/trash-D8zYUmTg.js +1 -0
- package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
- package/client/dist/assets/upload-CCHvUkRs.js +1 -0
- package/client/dist/assets/{useDesktop-DKNbOF0u.js → useDesktop-C5JVBeur.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-D3YMw9qx.js → useRuntimeRuns-DohBmeg8.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-CNOvmgTr.js +2 -0
- package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
- package/client/dist/assets/x-Bu7X_eR2.js +1 -0
- package/client/dist/assets/zap-bugHyWzQ.js +1 -0
- package/client/dist/index.html +44 -41
- package/docs/ci-cd.md +8 -1
- package/docs/customizing.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/guide/de/integrations/6-agent-chat.md +11 -0
- package/docs/guide/de/settings/5-project-environment.md +27 -0
- package/docs/guide/en/integrations/6-agent-chat.md +11 -0
- package/docs/guide/en/settings/5-project-environment.md +27 -0
- package/docs/guide/es/integrations/6-agent-chat.md +11 -0
- package/docs/guide/es/settings/5-project-environment.md +27 -0
- package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
- package/docs/guide/fr/settings/5-project-environment.md +27 -0
- package/docs/guide/it/integrations/6-agent-chat.md +11 -0
- package/docs/guide/it/settings/5-project-environment.md +27 -0
- package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
- package/docs/guide/ja/settings/5-project-environment.md +27 -0
- package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
- package/docs/guide/pt/settings/5-project-environment.md +27 -0
- package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
- package/docs/guide/zh/settings/5-project-environment.md +27 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/agent-sessions.md +157 -0
- package/docs/internals/api-reference.md +35 -4
- package/docs/internals/configuration.md +80 -5
- package/docs/internals/core-runtime-updates.md +10 -3
- package/docs/internals/operations-runbook.md +11 -0
- package/docs/internals/programmatic-agent-runtime.md +1 -1
- package/docs/internals/safe-pr-review-flow.md +15 -0
- package/docs/internals/source-map.md +42 -1
- package/docs/mcp.md +1 -1
- package/docs/platforms/macos.md +2 -0
- package/docs/platforms/windows-parity.md +4 -4
- package/docs/platforms/windows.md +9 -9
- package/local-runner/dist/specrails-local-runner.js +1 -1
- package/mcp-bridge/dist/specrails-mcp.js +82 -15
- package/package.json +8 -9
- package/server/dist/agent-mcp-config.js +41 -0
- package/server/dist/core-compat.js +2 -0
- package/server/dist/desktop-db.js +101 -4
- package/server/dist/desktop-router.js +31 -2
- package/server/dist/index.js +58 -0
- package/server/dist/mcp/tools/app-settings.js +11 -0
- package/server/dist/mcp/tools/env.js +38 -3
- package/server/dist/mcp/tools/mission.js +45 -2
- package/server/dist/mcp/tools/specs.js +2 -2
- package/server/dist/modules/accounting/runtime/result-event.js +9 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
- package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
- package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
- package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
- package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
- package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
- package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
- package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
- package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
- package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
- package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
- package/server/dist/modules/agent-sessions/index.js +31 -0
- package/server/dist/modules/agent-sessions/ports.js +2 -0
- package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
- package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
- package/server/dist/modules/agents/runtime/agent-store.js +3 -1
- package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
- package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +15 -4
- package/server/dist/modules/loops/runtime/loop-run-manager.js +11 -0
- package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
- package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
- package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
- package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
- package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
- package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
- package/server/dist/modules/project-settings/adapters/http.js +38 -2
- package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
- package/server/dist/modules/project-settings/domain.js +24 -0
- package/server/dist/modules/project-settings/index.js +2 -1
- package/server/dist/modules/specs/runtime/spec-models.js +1 -1
- package/server/dist/path-resolver.js +135 -20
- package/server/dist/plugins/codex-spawn.js +15 -7
- package/server/dist/project-env.js +265 -17
- package/server/dist/project-registry.js +46 -0
- package/server/dist/project-router-settings.js +24 -1
- package/server/dist/providers/claude-adapter.js +6 -1
- package/server/dist/worktree-node-modules.js +95 -1
- package/client/dist/assets/ActivityFeedPage-Wi5XNgoG.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-CXz2j-Bt.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DnsBPtpS.js +0 -2
- package/client/dist/assets/AgentsPage-CXrqu5vo.js +0 -118
- package/client/dist/assets/AnalyticsPage-Cvtt-LOO.js +0 -1
- package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
- package/client/dist/assets/CodePage-D35o-MFO.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-CsDFy5rp.js +0 -1
- package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-CFKvbQeJ.js +0 -19
- package/client/dist/assets/JobDetailPage-C6gpdYvj.js +0 -1
- package/client/dist/assets/JobsPage-CgS0pnKd.js +0 -1
- package/client/dist/assets/LoopBuilderPage-jUp20DSZ.js +0 -1
- package/client/dist/assets/LoopsPage-ClSrWzDd.js +0 -2
- package/client/dist/assets/MinimizedChatsContext-CroAs6Kh.js +0 -1
- package/client/dist/assets/PluginsPage-nCKQOwRL.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-BVDYwllU.js +0 -1
- package/client/dist/assets/RepositoryDeliveries-DJCm1pZr.js +0 -5
- package/client/dist/assets/ReviewPacketPage-kTErrEjx.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-YBJX3RmP.js +0 -16
- package/client/dist/assets/Trans-D5eOjfvG.js +0 -1
- package/client/dist/assets/agent--8QAfoXL.js +0 -1
- package/client/dist/assets/agent-3HxXkIWX.js +0 -1
- package/client/dist/assets/agent-CpFCz32T.js +0 -1
- package/client/dist/assets/agent-DVnw6ybO.js +0 -1
- package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
- package/client/dist/assets/agent-hv57-zyG.js +0 -1
- package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
- package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
- package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
- package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
- package/client/dist/assets/download-CFHyyHcw.js +0 -1
- package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
- package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
- package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
- package/client/dist/assets/index-Bfunvi6w.css +0 -2
- package/client/dist/assets/index-lTGJJTuk.js +0 -83
- package/client/dist/assets/jira-api-Bp0zXwjg.js +0 -1
- package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
- package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
- package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
- package/client/dist/assets/plus-CsnjfkmX.js +0 -1
- package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
- package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
- package/client/dist/assets/save-OsUqVn99.js +0 -1
- package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
- package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
- package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
- package/client/dist/assets/spending-DQDNtM8q.js +0 -1
- package/client/dist/assets/square-Ci-PFcFe.js +0 -1
- package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
- package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
- package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
- package/client/dist/assets/useSharedWebSocket-DaWuiO0S.js +0 -2
- package/client/dist/assets/x-CBMI9ejn.js +0 -1
- package/client/dist/assets/zap-CUOlvtT1.js +0 -1
|
@@ -47,6 +47,8 @@ function mapMessage(row) {
|
|
|
47
47
|
attachment_ids: ids,
|
|
48
48
|
context_refs: parseJsonArray(row.context_refs, isContextRef),
|
|
49
49
|
intents: parseIntents(row.intent),
|
|
50
|
+
...(row.core_turn_id ? { core_turn_id: row.core_turn_id } : {}),
|
|
51
|
+
...(row.turn_origin === 'user' || row.turn_origin === 'subagent' || row.turn_origin === 'system' ? { turn_origin: row.turn_origin } : {}),
|
|
50
52
|
created_at: row.created_at,
|
|
51
53
|
};
|
|
52
54
|
}
|
|
@@ -128,7 +130,7 @@ function addAgentMessage(db, input) {
|
|
|
128
130
|
const id = (0, crypto_1.randomUUID)();
|
|
129
131
|
const attachmentIds = input.attachmentIds && input.attachmentIds.length ? JSON.stringify(input.attachmentIds) : null;
|
|
130
132
|
const contextRefs = input.contextRefs && input.contextRefs.length ? JSON.stringify(input.contextRefs) : null;
|
|
131
|
-
db.prepare('INSERT INTO agent_messages (id, conversation_id, role, content, attachment_ids, context_refs) VALUES (?, ?, ?, ?, ?, ?)').run(id, input.conversationId, input.role, input.content, attachmentIds, contextRefs);
|
|
133
|
+
db.prepare('INSERT INTO agent_messages (id, conversation_id, role, content, attachment_ids, context_refs, turn_origin, core_turn_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, input.conversationId, input.role, input.content, attachmentIds, contextRefs, input.turnOrigin ?? null, input.coreTurnId ?? null);
|
|
132
134
|
// Touch the parent so the list stays ordered by latest activity.
|
|
133
135
|
db.prepare("UPDATE agent_conversations SET updated_at = datetime('now') WHERE id = ?").run(input.conversationId);
|
|
134
136
|
return mapMessage(db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id));
|
|
@@ -2217,8 +2217,12 @@ class QueueManager {
|
|
|
2217
2217
|
// values are read from the server env (with login-shell recovery for missing
|
|
2218
2218
|
// names) at spawn time. Apply this before Specrails' own env overlays so
|
|
2219
2219
|
// internal SPECRAILS_* control-plane values always win.
|
|
2220
|
+
let environmentNotice = null;
|
|
2220
2221
|
if (this._db) {
|
|
2221
2222
|
spawnEnv = (0, project_env_1.applyWorktreeEnvPassthrough)(this._db, spawnEnv);
|
|
2223
|
+
// One value-free line naming unresolved configured names (written to the
|
|
2224
|
+
// job log once the readers exist). The run proceeds without them.
|
|
2225
|
+
environmentNotice = (0, project_env_1.worktreeEnvPassthroughWarning)(this._db, spawnEnv);
|
|
2222
2226
|
}
|
|
2223
2227
|
const telemetryEnabled = !!(this._projectId && this._db && (0, db_1.getProjectSettings)(this._db).pipelineTelemetryEnabled);
|
|
2224
2228
|
// Resolve the framework version ONCE at spawn time — `framework/current`
|
|
@@ -2605,6 +2609,10 @@ class QueueManager {
|
|
|
2605
2609
|
const stdoutReader = (0, readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
|
|
2606
2610
|
const stderrReader = (0, readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
|
|
2607
2611
|
this._jobReaders.set(jobId, { stdout: stdoutReader, stderr: stderrReader });
|
|
2612
|
+
if (environmentNotice) {
|
|
2613
|
+
persistEvent({ event_type: 'log', source: 'stderr', payload: JSON.stringify({ line: environmentNotice }) }, false);
|
|
2614
|
+
emitLine('stderr', environmentNotice);
|
|
2615
|
+
}
|
|
2608
2616
|
stdoutReader.on('line', (line) => {
|
|
2609
2617
|
if (this._disposed)
|
|
2610
2618
|
return;
|
|
@@ -181,6 +181,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
181
181
|
const completionContexts = new Map();
|
|
182
182
|
const runtimeConfigPath = (cwd) => (0, node_path_1.join)(opts.pluginScope?.().stateRoot ?? cwd, '.specrails', 'agent-runtime.json');
|
|
183
183
|
return {
|
|
184
|
+
...(opts.environmentNotice ? { environmentNotice: opts.environmentNotice } : {}),
|
|
184
185
|
async cancelDefinition({ runId, contextPath, requestId }) {
|
|
185
186
|
const host = (0, agent_runtime_bridge_1.readFrozenRuntimeHost)(contextPath, resolveEnv(), runId);
|
|
186
187
|
await (0, agent_runtime_bridge_1.runAgentRuntimeControl)({ kind: 'cancel', runId, contextPath, requestId, ...host });
|
|
@@ -48,10 +48,21 @@ function configurableImplementGraph(capabilities) {
|
|
|
48
48
|
node('reset-confidence', 'Reset confidence after answer', 'assign', { set: { planInvestigated: false } }, { next: 'architect', failed: 'failed' });
|
|
49
49
|
node('validate', 'Validate OpenSpec', 'openspec-validate', { change }, { pass: 'freeze', fail: 'architect', failed: 'failed' });
|
|
50
50
|
node('freeze', 'Freeze approved artifacts', 'artifact-contract', { change, contractId: 'plan', action: 'freeze' }, { pass: 'initialize-corrections', fail: 'architect', failed: 'failed' });
|
|
51
|
-
node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3 } }, { next: 'developer', failed: 'failed' });
|
|
51
|
+
node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3, implementationAttempts: 0, implementationLimit: 3 } }, { next: 'developer', failed: 'failed' });
|
|
52
52
|
const implementation = objectSchema({ summary: string, incomplete: strings, blocker }, ['summary', 'incomplete']);
|
|
53
|
-
turn('developer', 'Developer', 'build', `Implement every approved task for change ${change} using the bound official apply skill. Preserve proposal, design and specs; only tick tasks you completed. Report summary and incomplete task descriptions. Prior execution evidence and human answers: {{history.text}}`, implementation, '
|
|
54
|
-
node('
|
|
53
|
+
turn('developer', 'Developer', 'build', `Implement every approved task for change ${change} using the bound official apply skill. Preserve proposal, design and specs; only tick tasks you completed. Report summary and incomplete task descriptions. If a task cannot be finished because of a host precondition or environment blocker outside this change (for example the sandbox forbids starting a local server, a credential or tool is missing), keep the work done, leave that task unchecked and return the structured blocker (kind, command, cwd, evidence, requiredAction). A failing test, a test you cannot make pass, or unexpected browser or tool behaviour is NOT a host blocker: change the test or the implementation instead. The host confirms a blocker by running its own verification before ending the run; a failure it reproduces comes back as a correction. Prior execution evidence and human answers: {{history.text}}`, implementation, 'developer-blocker', writes);
|
|
54
|
+
node('developer-blocker', 'Developer reported a host blocker', 'condition', { expr: 'exists($outputs.developer.structured.blocker) && $outputs.developer.structured.blocker != null' }, { true: 'developer-blocker-kind', false: 'tasks' });
|
|
55
|
+
// Credentials, network and variables are the person's to provide. Any other
|
|
56
|
+
// blocker is confirmed by the host's own verification: a host precondition
|
|
57
|
+
// ends at host-blocked, a reproduced failure becomes a correction, and
|
|
58
|
+
// passing checks reach the reviewer, whose rejection of unfinished tasks is
|
|
59
|
+
// corrected within the same budget.
|
|
60
|
+
node('developer-blocker-kind', 'Blocker needs the person', 'condition', { expr: ['credential', 'network', 'environment-variable'].map(kind => `$outputs.developer.structured.blocker.kind == "${kind}"`).join(' || ') }, { true: 'developer-blocked', false: 'verify' });
|
|
61
|
+
node('tasks', 'Check artifacts and completed tasks', 'artifact-contract', { change, contractId: 'plan', action: 'check', requireCompletedTasks: true }, { pass: 'verify', fail: 'implementation-budget', failed: 'failed' });
|
|
62
|
+
// Incomplete tasks send the developer back, but only while each attempt changes the candidate and within budget.
|
|
63
|
+
node('implementation-budget', 'Check implementation attempt budget', 'condition', { expr: '$vars.implementationAttempts < $vars.implementationLimit' }, { true: 'implementation-progress', false: 'implementation-exhausted' });
|
|
64
|
+
node('implementation-progress', 'Check implementation progress', 'condition', { expr: '!exists($vars.lastImplementationHash) || $outputs.developer.candidateHash != $vars.lastImplementationHash' }, { true: 'begin-implementation', false: 'implementation-stalled' });
|
|
65
|
+
node('begin-implementation', 'Count implementation attempt', 'assign', { increment: { implementationAttempts: 1 }, set: { lastImplementationHash: '{{outputs.developer.candidateHash}}' } }, { next: 'developer', failed: 'failed' });
|
|
55
66
|
node('verify', 'Verify candidate', 'verify', verifyParams, verifyEnds('reviewer', 'correction-context', 'host-blocked'));
|
|
56
67
|
turn('reviewer', 'Reviewer', 'assess', `Review change ${change} read-only against all approved artifacts and frozen ticket acceptance criteria. Inspect real code and the host verification evidence: {{outputs.verify}}. Return approved, summary, issues, score (0-100), and aspects. Acceptance policy: ${reviewPolicy}. For any rejection or below-threshold score, explain the concrete unmet requirement and actionable repair in summary and issues; passing commands do not prove every acceptance obligation. Prior execution: {{history.text}}`, objectSchema({ approved: { type: 'boolean' }, summary: string, issues: strings, score, aspects: objectSchema(Object.fromEntries(Object.keys(aspects).map(aspect => [aspect, score]))) }), 'review-policy');
|
|
57
68
|
node('review-policy', 'Review acceptance policy', 'condition', { expr: ['$outputs.reviewer.structured.approved == true', '$outputs.reviewer.structured.issues.length == 0', '$outputs.reviewer.structured.score >= 70', ...Object.entries(aspects).map(([aspect, threshold]) => `$outputs.reviewer.structured.aspects.${aspect} >= ${threshold}`)].join(' && ') }, { true: 'approve', false: 'correction-context' });
|
|
@@ -67,7 +78,7 @@ function configurableImplementGraph(capabilities) {
|
|
|
67
78
|
node('approval-policy', 'Approval decision', 'condition', { expr: '$outputs.approve.response.approved == true' }, { true: 'archive', false: 'failed' });
|
|
68
79
|
node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'verify' });
|
|
69
80
|
node('verify-archive', 'Verify archived candidate', 'verify', verifyParams, verifyEnds('done', 'failed', 'host-blocked-archive'));
|
|
70
|
-
nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction made no candidate changes. Host verification is valid: {{outputs.verify.valid}}. Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}` } }, { id: 'correction-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction limit reached ({{run.correctionAttempts}}/{{run.correctionLimit}}). Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}. Host verification: {{outputs.verify}}` } }, { id: 'fixer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Fixer reported a host blocker ({{outputs.fixer.structured.blocker.kind}}): {{outputs.fixer.structured.blocker.requiredAction}} Evidence: {{outputs.fixer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'fixer' } : {}) } });
|
|
81
|
+
nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction made no candidate changes. Host verification is valid: {{outputs.verify.valid}}. Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}` } }, { id: 'correction-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction limit reached ({{run.correctionAttempts}}/{{run.correctionLimit}}). Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}. Host verification: {{outputs.verify}}` } }, { id: 'implementation-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'The developer made no changes while approved tasks are still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'implementation-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Implementation attempt limit reached ({{run.implementationAttempts}}/{{run.implementationLimit}}) with approved tasks still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'developer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Developer reported a host blocker ({{outputs.developer.structured.blocker.kind}}): {{outputs.developer.structured.blocker.requiredAction}} Evidence: {{outputs.developer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'developer' } : {}) } }, { id: 'fixer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Fixer reported a host blocker ({{outputs.fixer.structured.blocker.kind}}): {{outputs.fixer.structured.blocker.requiredAction}} Evidence: {{outputs.fixer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'fixer' } : {}) } });
|
|
71
82
|
if (hostBlockers) {
|
|
72
83
|
const hostBlocked = (id, source) => ({ id, type: 'end', position: { x: 1800, y: nodes.length * 140 }, data: { outcome: 'failure', blockerFrom: source, reason: `Host blocker ({{outputs.${source}.blocker.kind}}): {{outputs.${source}.blocker.reason}} Required action: {{outputs.${source}.blocker.requiredAction}}` } });
|
|
73
84
|
nodes.push(hostBlocked('host-blocked', 'verify'), hostBlocked('host-blocked-archive', 'verify-archive'));
|
|
@@ -756,6 +756,10 @@ class LoopRunManager {
|
|
|
756
756
|
throw new Error('runtime_run_completed: Fork a completed workflow to continue');
|
|
757
757
|
this._cancelled.delete(runId);
|
|
758
758
|
this.claimDefinition({ ...frozen.request, runId });
|
|
759
|
+
// Cards and chips that showed the restart/interruption pause follow the resumed run live.
|
|
760
|
+
const request = frozen.request;
|
|
761
|
+
this._emit({ type: 'loop.run_resumed', projectId: request.projectId, loopRunId: runId, railIndex: request.railIndex ?? null,
|
|
762
|
+
ticketIds: request.spec?.ticketIds ?? (request.ticketId == null ? [] : [request.ticketId]) });
|
|
759
763
|
const task = this._run(frozen.request, { resume: true, ...input }).finally(() => { this.releaseDefinition(runId); this._definitionTasks.delete(runId); });
|
|
760
764
|
this._definitionTasks.set(runId, task);
|
|
761
765
|
return task;
|
|
@@ -1005,6 +1009,13 @@ class LoopRunManager {
|
|
|
1005
1009
|
logLine(`▶ Loop "${req.loopName ?? req.loopId}" started${req.spec?.title ? ` — spec: ${req.spec.title}` : ''}`);
|
|
1006
1010
|
if (req.isolation)
|
|
1007
1011
|
logLine(`⎇ Isolated worktree: ${req.isolation.worktreePath} (branch ${req.isolation.branch})`);
|
|
1012
|
+
let environmentNotice = null;
|
|
1013
|
+
try {
|
|
1014
|
+
environmentNotice = this.executors.environmentNotice?.() ?? null;
|
|
1015
|
+
}
|
|
1016
|
+
catch { /* diagnostic only */ }
|
|
1017
|
+
if (environmentNotice)
|
|
1018
|
+
logLine(environmentNotice, 'stderr');
|
|
1008
1019
|
if (!definitionEngine && !usageTelemetryAvailable) {
|
|
1009
1020
|
logLine(`⚠️ ${adapter.displayName} does not report token/cost usage in headless mode; usage totals will remain unavailable.` +
|
|
1010
1021
|
(configuredMaxCostUsd !== undefined
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AgentChatManager = void 0;
|
|
6
|
+
exports.AgentChatManager = exports.SessionRebuildBusyError = void 0;
|
|
7
7
|
exports.sanitizeAgentTitle = sanitizeAgentTitle;
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
9
|
const readline_1 = require("readline");
|
|
@@ -35,6 +35,8 @@ const agent_store_1 = require("../../agents/runtime/agent-store");
|
|
|
35
35
|
const explore_draft_title_1 = require("../../conversations/runtime/explore-draft-title");
|
|
36
36
|
const agent_input_store_1 = require("./agent-input-store");
|
|
37
37
|
const agent_steering_1 = require("./agent-steering");
|
|
38
|
+
const core_session_runner_1 = require("./core-session-runner");
|
|
39
|
+
const agent_session_store_1 = require("./agent-session-store");
|
|
38
40
|
/**
|
|
39
41
|
* Providers report an expired or foreign resume token as a normalized error.
|
|
40
42
|
* Those errors are safe to recover from by clearing the token and retrying the
|
|
@@ -47,12 +49,18 @@ function isStaleResumeError(message) {
|
|
|
47
49
|
/unknown (?:session|thread)(?: id)?\b/i.test(message) ||
|
|
48
50
|
/(?:session|thread)(?: id)?\b[^\n]{0,160}\b(?:not found|does not exist)\b/i.test(message));
|
|
49
51
|
}
|
|
52
|
+
/** A projection rebuild was requested while a turn is running. */
|
|
53
|
+
class SessionRebuildBusyError extends Error {
|
|
54
|
+
}
|
|
55
|
+
exports.SessionRebuildBusyError = SessionRebuildBusyError;
|
|
50
56
|
class AgentChatManager {
|
|
51
57
|
_broadcast;
|
|
52
58
|
_db;
|
|
53
59
|
_port;
|
|
54
60
|
_registry;
|
|
55
61
|
_active = new Map();
|
|
62
|
+
/** Missions' Core agent sessions (null = legacy transports only). */
|
|
63
|
+
_coreSessions = null;
|
|
56
64
|
_nativeInputNotifiers = new Map();
|
|
57
65
|
/** Conversations with a turn in-flight but not yet spawned. Closes the TOCTOU
|
|
58
66
|
* window the attachment-extraction await opens between the busy guard and
|
|
@@ -89,6 +97,21 @@ class AgentChatManager {
|
|
|
89
97
|
// A sidecar restart must retain unsent input without replaying operations.
|
|
90
98
|
(0, agent_input_store_1.recoverPendingAgentInputs)(db);
|
|
91
99
|
}
|
|
100
|
+
/** Composition-time wiring of Core agent sessions (see agent-sessions module). */
|
|
101
|
+
setCoreSessions(coreSessions) {
|
|
102
|
+
this._coreSessions = coreSessions;
|
|
103
|
+
}
|
|
104
|
+
/** Conversation deleted: close its Core session (reads the row before it is dropped). */
|
|
105
|
+
closeCoreSession(conversationId) {
|
|
106
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
|
|
107
|
+
if (!conversation || !this._coreSessions)
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
return this._coreSessions.close(conversation);
|
|
110
|
+
}
|
|
111
|
+
/** A turn is live while its provider child or its Core session turn is running. */
|
|
112
|
+
_isTurnLive(conversationId) {
|
|
113
|
+
return this._active.has(conversationId) || !!this._coreSessions?.handle(conversationId);
|
|
114
|
+
}
|
|
92
115
|
async _awaitWhileLive(work) {
|
|
93
116
|
if (this._disposed)
|
|
94
117
|
return { disposed: true };
|
|
@@ -160,11 +183,11 @@ class AgentChatManager {
|
|
|
160
183
|
}
|
|
161
184
|
/** True while a turn is streaming for this conversation. */
|
|
162
185
|
isStreaming(conversationId) {
|
|
163
|
-
return !this._disposed && this.
|
|
186
|
+
return !this._disposed && this._isTurnLive(conversationId);
|
|
164
187
|
}
|
|
165
188
|
/** True while a turn is in flight (spawned or reserved). */
|
|
166
189
|
isBusy(conversationId) {
|
|
167
|
-
return !this._disposed && (this.
|
|
190
|
+
return !this._disposed && (this._isTurnLive(conversationId) || this._reserved.has(conversationId));
|
|
168
191
|
}
|
|
169
192
|
pendingMessages(conversationId) {
|
|
170
193
|
return (this._queue.get(conversationId) ?? []).map((item) => ({
|
|
@@ -192,8 +215,62 @@ class AgentChatManager {
|
|
|
192
215
|
pendingMessages: this.pendingMessages(conversationId),
|
|
193
216
|
streamingText: this._streamingText.get(conversationId) ?? '',
|
|
194
217
|
})),
|
|
218
|
+
// Core agent sessions with background work: the client restores the
|
|
219
|
+
// background indicator and live sub-agent rows from this after reconnecting.
|
|
220
|
+
sessions: (0, agent_session_store_1.listActiveSessionCursors)(this._db).map((cursor) => ({
|
|
221
|
+
conversationId: cursor.conversationId,
|
|
222
|
+
residentPhase: cursor.residentPhase,
|
|
223
|
+
processAlive: cursor.processAlive,
|
|
224
|
+
liveSubagents: cursor.liveSubagents,
|
|
225
|
+
subagents: (0, agent_session_store_1.listSubagents)(this._db, cursor.conversationId).filter((node) => node.phase === 'running'),
|
|
226
|
+
})),
|
|
195
227
|
};
|
|
196
228
|
}
|
|
229
|
+
/** Projected Core session state of one mission (null when it never ran in Core). */
|
|
230
|
+
sessionState(conversationId) {
|
|
231
|
+
const cursor = (0, agent_session_store_1.getSessionCursor)(this._db, conversationId);
|
|
232
|
+
if (!cursor)
|
|
233
|
+
return null;
|
|
234
|
+
return { residentPhase: cursor.residentPhase, processAlive: cursor.processAlive, liveSubagents: cursor.liveSubagents, subagents: (0, agent_session_store_1.listSubagents)(this._db, conversationId) };
|
|
235
|
+
}
|
|
236
|
+
/** Stop a mission's background sub-agents through its Core session. */
|
|
237
|
+
async stopSubagents(conversationId, subagentIds) {
|
|
238
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
|
|
239
|
+
if (!conversation || !this._coreSessions)
|
|
240
|
+
return [];
|
|
241
|
+
return this._coreSessions.stopSubagents(conversation, subagentIds);
|
|
242
|
+
}
|
|
243
|
+
/** Delegated runtime: launch a sub-agent on the mission's configured provider. */
|
|
244
|
+
async delegateSubagent(conversationId, params) {
|
|
245
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
|
|
246
|
+
if (!conversation || !this._coreSessions)
|
|
247
|
+
throw new Error('This mission does not run in a Core session.');
|
|
248
|
+
return this._coreSessions.delegate(conversation, params);
|
|
249
|
+
}
|
|
250
|
+
/** Delegated runtime: wait for sub-agents (results are delivered to the agent once). */
|
|
251
|
+
async waitSubagents(conversationId, subagentIds, timeoutMs) {
|
|
252
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
|
|
253
|
+
if (!conversation || !this._coreSessions)
|
|
254
|
+
throw new Error('This mission does not run in a Core session.');
|
|
255
|
+
return this._coreSessions.waitSubagents(conversation, subagentIds, timeoutMs);
|
|
256
|
+
}
|
|
257
|
+
/** Plugin MCP servers for a project-pinned mission (empty for providers without plugin MCP entries). */
|
|
258
|
+
_pluginMcpServers(providerId, projectId) {
|
|
259
|
+
const project = projectId ? this._registry?.getProjectRow?.(projectId) : null;
|
|
260
|
+
if (!project)
|
|
261
|
+
return [];
|
|
262
|
+
const execution = (0, workspace_resolution_1.resolveProjectExecution)(project);
|
|
263
|
+
return (0, codex_spawn_1.resolveCodexPluginMcpServers)({ providerId, stateRoot: execution.cwd, repositoryPath: project.path, legacyProviderId: project.provider });
|
|
264
|
+
}
|
|
265
|
+
/** Rebuild a mission's session projection from Core's journal (operator repair). */
|
|
266
|
+
async rebuildSessionProjection(conversationId) {
|
|
267
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
|
|
268
|
+
if (!conversation || !this._coreSessions)
|
|
269
|
+
return null;
|
|
270
|
+
if (this._isTurnLive(conversationId))
|
|
271
|
+
throw new SessionRebuildBusyError('Wait for the current turn to finish before rebuilding');
|
|
272
|
+
return this._coreSessions.rebuildProjection(conversation);
|
|
273
|
+
}
|
|
197
274
|
notifyConversationCreated(conversationId) {
|
|
198
275
|
this._broadcast({ type: 'agent_conversation_created', conversationId, timestamp: new Date().toISOString() });
|
|
199
276
|
}
|
|
@@ -478,25 +555,31 @@ class AgentChatManager {
|
|
|
478
555
|
const invocationId = (0, crypto_1.randomUUID)();
|
|
479
556
|
currentCapability = agentCapability;
|
|
480
557
|
currentInvocationId = invocationId;
|
|
481
|
-
let mcpArgs;
|
|
482
|
-
let mcpEnv;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
558
|
+
let mcpArgs = [];
|
|
559
|
+
let mcpEnv = {};
|
|
560
|
+
// Core agent sessions: a resident session presents this turn's capability
|
|
561
|
+
// through structured MCP specs (no provider-specific argv).
|
|
562
|
+
const coreContext = this._coreSessions
|
|
563
|
+
? await this._coreSessions.prepareTurn(conversation, adapter, { capability: agentCapability, external: (0, external_mcp_1.resolveExternalEntries)(adapter.id, this._db), plugins: this._pluginMcpServers(adapter.id, conversation.pinned_project_id) })
|
|
564
|
+
: null;
|
|
565
|
+
if (!coreContext)
|
|
566
|
+
try {
|
|
567
|
+
const wiring = (0, agent_mcp_config_1.prepareAgentMcp)({
|
|
568
|
+
adapterId: adapter.id, conversationId, invocationId, cwd,
|
|
569
|
+
port: this._port, capability: agentCapability,
|
|
570
|
+
external: (0, external_mcp_1.resolveExternalEntries)(adapter.id, this._db),
|
|
571
|
+
});
|
|
572
|
+
mcpArgs = wiring.extraArgs;
|
|
573
|
+
mcpEnv = wiring.env;
|
|
574
|
+
const project = conversation.pinned_project_id ? this._registry?.getProjectRow?.(conversation.pinned_project_id) : null;
|
|
575
|
+
if (adapter.id === 'codex' && project) {
|
|
576
|
+
const execution = (0, workspace_resolution_1.resolveProjectExecution)(project);
|
|
577
|
+
mcpArgs.push(...(0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: execution.cwd, repositoryPath: project.path, legacyProviderId: project.provider }));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
throw new Error('Specrails could not prepare its MCP tools. The agent was not launched. Check the app installation and retry. ' + (err instanceof Error ? err.message : String(err)));
|
|
495
582
|
}
|
|
496
|
-
}
|
|
497
|
-
catch (err) {
|
|
498
|
-
throw new Error('Specrails could not prepare its MCP tools. The agent was not launched. Check the app installation and retry. ' + (err instanceof Error ? err.message : String(err)));
|
|
499
|
-
}
|
|
500
583
|
const action = useResume ? 'chat-resume' : 'chat-turn';
|
|
501
584
|
const startedAt = new Date().toISOString();
|
|
502
585
|
let streamed = '';
|
|
@@ -505,7 +588,7 @@ class AgentChatManager {
|
|
|
505
588
|
let capturedSessionId = useResume ? conversation.session_id ?? null : null;
|
|
506
589
|
let capturedError = null;
|
|
507
590
|
console.log(`[agent-chat] turn start conv=${conversationId} provider=${adapter.id} binary=${adapter.binary} ` +
|
|
508
|
-
`action=${action} model=${model} cwd=${cwd} mcp=${mcpArgs.length ? 'mcp-config' : Object.keys(mcpEnv).join(',') || 'none'}`);
|
|
591
|
+
`action=${action} model=${model} cwd=${cwd} mcp=${coreContext ? `core-session:${coreContext.mcpServers.map((server) => server.name).join(',') || 'none'}` : mcpArgs.length ? 'mcp-config' : Object.keys(mcpEnv).join(',') || 'none'}`);
|
|
509
592
|
const buildOpts = {
|
|
510
593
|
prompt: effectivePrompt(useResume),
|
|
511
594
|
systemPrompt: adapter.capabilities.systemPromptArg ? systemPrompt : undefined,
|
|
@@ -515,7 +598,7 @@ class AgentChatManager {
|
|
|
515
598
|
imagePaths: spawnImagePaths,
|
|
516
599
|
reasoning_effort: reasoningEffort,
|
|
517
600
|
};
|
|
518
|
-
const nativeRunner = (0, live_session_1.nativeLiveSessionRunner)(adapter);
|
|
601
|
+
const nativeRunner = coreContext ? (0, core_session_runner_1.createCoreSessionRunner)(coreContext) : (0, live_session_1.nativeLiveSessionRunner)(adapter);
|
|
519
602
|
let invocationLive = true;
|
|
520
603
|
let nativeSink;
|
|
521
604
|
let nativeDrain = Promise.resolve();
|
|
@@ -782,13 +865,13 @@ class AgentChatManager {
|
|
|
782
865
|
// A killed child (abort / conversation DELETE) keeps flushing its
|
|
783
866
|
// buffered stdout — suppress the broadcasts once it left _active
|
|
784
867
|
// so stragglers can't resurrect client-side streaming state.
|
|
785
|
-
if (this.
|
|
868
|
+
if (this._isTurnLive(conversationId)) {
|
|
786
869
|
this._streamingText.set(conversationId, streamed.slice(segmentStart));
|
|
787
870
|
this._broadcast({ type: 'agent_stream', conversationId, delta: ev.text, timestamp: timestamp() });
|
|
788
871
|
}
|
|
789
872
|
break;
|
|
790
873
|
case 'tool-use':
|
|
791
|
-
if (this.
|
|
874
|
+
if (this._isTurnLive(conversationId)) {
|
|
792
875
|
this._broadcast({
|
|
793
876
|
type: 'agent_tool',
|
|
794
877
|
conversationId,
|
|
@@ -802,7 +885,7 @@ class AgentChatManager {
|
|
|
802
885
|
case 'tool-result':
|
|
803
886
|
// Feeds the activity-log modal's output column. Claude-only
|
|
804
887
|
// today; other adapters never emit this kind.
|
|
805
|
-
if (this.
|
|
888
|
+
if (this._isTurnLive(conversationId)) {
|
|
806
889
|
this._broadcast({
|
|
807
890
|
type: 'agent_tool_result',
|
|
808
891
|
conversationId,
|
|
@@ -845,7 +928,10 @@ class AgentChatManager {
|
|
|
845
928
|
this._nativeInputNotifiers.delete(conversationId);
|
|
846
929
|
stopSteering();
|
|
847
930
|
this._steeringCapabilities.delete(conversationId);
|
|
848
|
-
|
|
931
|
+
// A resident Core session keeps presenting this capability for its
|
|
932
|
+
// background turns until the next turn rotates it or Core retires it.
|
|
933
|
+
if (!this._coreSessions?.retains(conversationId, agentCapability))
|
|
934
|
+
(0, agent_capability_1.revokeAgentCapability)(agentCapability);
|
|
849
935
|
(0, agent_mcp_config_1.removeAgentCapabilityFile)(conversationId, invocationId);
|
|
850
936
|
currentCapability = null;
|
|
851
937
|
currentInvocationId = undefined;
|
|
@@ -874,7 +960,8 @@ class AgentChatManager {
|
|
|
874
960
|
if (result.stderrTail && (result.spawnFailed || (result.code ?? 0) !== 0 || !text)) {
|
|
875
961
|
console.error(`[agent-chat] ${adapter.id} stderr:\n${result.stderrTail}`);
|
|
876
962
|
}
|
|
877
|
-
|
|
963
|
+
const coreResult = result;
|
|
964
|
+
return { disposed: false, text, fullText: streamed.trim(), checkpointed, sessionId: capturedSessionId, error: capturedError, code: result.code, spawnFailed: result.spawnFailed, stderrTail: result.stderrTail, events: result.events, startedAt, ...(coreResult.usage ? { usage: coreResult.usage } : {}), ...(coreResult.coreTurnId ? { coreTurnId: coreResult.coreTurnId } : {}), ...(coreResult.errorCode ? { errorCode: coreResult.errorCode } : {}) };
|
|
878
965
|
};
|
|
879
966
|
// Conversation CONFIG (provider/model/tier) is owned by the PATCH route —
|
|
880
967
|
// never write turn-START snapshots back at settle, or a mid-turn provider
|
|
@@ -896,9 +983,9 @@ class AgentChatManager {
|
|
|
896
983
|
// Deliberate user Stop: keep any partial text (it was already streamed to
|
|
897
984
|
// the client), never auto-heal, never surface an error.
|
|
898
985
|
if (r.text && (0, agent_store_1.getAgentConversation)(this._db, conversationId)) {
|
|
899
|
-
const message = (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text });
|
|
986
|
+
const message = (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text, ...(r.coreTurnId ? { coreTurnId: r.coreTurnId, turnOrigin: 'user' } : {}) });
|
|
900
987
|
persistSession(r.sessionId);
|
|
901
|
-
this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, messageId: message.id, timestamp: timestamp() });
|
|
988
|
+
this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, messageId: message.id, ...(r.coreTurnId ? { coreTurnId: r.coreTurnId } : {}), timestamp: timestamp() });
|
|
902
989
|
}
|
|
903
990
|
};
|
|
904
991
|
// Cost accounting (HIGH-3): exactly one row per settled turn, whichever
|
|
@@ -964,14 +1051,15 @@ class AgentChatManager {
|
|
|
964
1051
|
const message = (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text });
|
|
965
1052
|
this._broadcast({ type: 'agent_partial', conversationId, fullText: r.text, messageId: message.id, error: reason, timestamp: timestamp() });
|
|
966
1053
|
}
|
|
967
|
-
this._emitError(conversationId, reason);
|
|
1054
|
+
this._emitError(conversationId, reason, r.errorCode ? { code: r.errorCode, provider: adapter.id } : undefined);
|
|
968
1055
|
record(r, 'failed');
|
|
969
1056
|
return;
|
|
970
1057
|
}
|
|
971
1058
|
if (r.text || r.checkpointed) {
|
|
972
|
-
const message = r.text ? (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text }) : undefined;
|
|
1059
|
+
const message = r.text ? (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text, ...(r.coreTurnId ? { coreTurnId: r.coreTurnId, turnOrigin: 'user' } : {}) }) : undefined;
|
|
973
1060
|
persistSession(r.sessionId);
|
|
974
|
-
|
|
1061
|
+
// The Core turn id anchors this turn's sub-agents under the reply in every client.
|
|
1062
|
+
this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, ...(message ? { messageId: message.id } : {}), ...(r.coreTurnId ? { coreTurnId: r.coreTurnId } : {}), timestamp: timestamp() });
|
|
975
1063
|
record(r, 'success');
|
|
976
1064
|
// AI title after the FIRST completed turn (industry standard — ChatGPT /
|
|
977
1065
|
// Claude.ai), on the conversation's own provider. The deterministic title
|
|
@@ -989,7 +1077,7 @@ class AgentChatManager {
|
|
|
989
1077
|
}
|
|
990
1078
|
finally {
|
|
991
1079
|
this._steeringCapabilities.delete(conversationId);
|
|
992
|
-
if (currentCapability)
|
|
1080
|
+
if (currentCapability && !this._coreSessions?.retains(conversationId, currentCapability))
|
|
993
1081
|
(0, agent_capability_1.revokeAgentCapability)(currentCapability);
|
|
994
1082
|
if (currentInvocationId)
|
|
995
1083
|
(0, agent_mcp_config_1.removeAgentCapabilityFile)(conversationId, currentInvocationId);
|
|
@@ -1009,10 +1097,11 @@ class AgentChatManager {
|
|
|
1009
1097
|
return;
|
|
1010
1098
|
try {
|
|
1011
1099
|
const finishedAt = new Date().toISOString();
|
|
1012
|
-
const {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1100
|
+
const finaliseOptions = { fallbackModel: model, durationMs: Math.max(0, Date.parse(finishedAt) - Date.parse(outcome.startedAt)) };
|
|
1101
|
+
// Core sessions report a normalized per-turn delta; never reparse provider frames for them.
|
|
1102
|
+
const { result, estimated } = outcome.usage
|
|
1103
|
+
? (0, result_event_1.finaliseNormalisedResult)(adapter, outcome.usage, finaliseOptions)
|
|
1104
|
+
: (0, result_event_1.finaliseInvocationResult)(adapter, outcome.events, finaliseOptions);
|
|
1016
1105
|
(0, desktop_db_1.recordAgentInvocation)(this._db, {
|
|
1017
1106
|
id: (0, crypto_1.randomUUID)(),
|
|
1018
1107
|
conversation_id: conversation.id,
|
|
@@ -1437,6 +1526,12 @@ class AgentChatManager {
|
|
|
1437
1526
|
(0, agent_capability_1.revokeAgentCapability)(capability);
|
|
1438
1527
|
this._steeringCapabilities.delete(conversationId);
|
|
1439
1528
|
this._nativeInputNotifiers.delete(conversationId);
|
|
1529
|
+
const coreTurn = this._coreSessions?.handle(conversationId);
|
|
1530
|
+
if (coreTurn) {
|
|
1531
|
+
// Stop the turn inside the resident session; never kill a process by PID.
|
|
1532
|
+
void coreTurn.interrupt();
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1440
1535
|
const child = this._active.get(conversationId);
|
|
1441
1536
|
if (!child)
|
|
1442
1537
|
return false;
|
|
@@ -1478,6 +1573,7 @@ class AgentChatManager {
|
|
|
1478
1573
|
this._closeChildIo(child);
|
|
1479
1574
|
}
|
|
1480
1575
|
this._active.clear();
|
|
1576
|
+
this._coreSessions?.shutdown();
|
|
1481
1577
|
for (const child of this._auxProcesses) {
|
|
1482
1578
|
const reader = this._auxReaders.get(child);
|
|
1483
1579
|
try {
|
|
@@ -1490,10 +1586,11 @@ class AgentChatManager {
|
|
|
1490
1586
|
this._auxReaders.clear();
|
|
1491
1587
|
this._auxProcesses.clear();
|
|
1492
1588
|
}
|
|
1493
|
-
|
|
1589
|
+
/** `detail.code` lets clients explain known failures (e.g. `policy_unenforceable`) in the user's language. */
|
|
1590
|
+
_emitError(conversationId, error, detail) {
|
|
1494
1591
|
if (this._disposed)
|
|
1495
1592
|
return;
|
|
1496
|
-
this._broadcast({ type: 'agent_error', conversationId, error, timestamp: new Date().toISOString() });
|
|
1593
|
+
this._broadcast({ type: 'agent_error', conversationId, error, ...(detail ?? {}), timestamp: new Date().toISOString() });
|
|
1497
1594
|
}
|
|
1498
1595
|
}
|
|
1499
1596
|
exports.AgentChatManager = AgentChatManager;
|
|
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.isAgentChatEnabled = isAgentChatEnabled;
|
|
7
7
|
exports.createAgentChatRouter = createAgentChatRouter;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const agent_session_store_1 = require("./agent-session-store");
|
|
9
10
|
const multer_1 = __importDefault(require("multer"));
|
|
10
11
|
const express_1 = require("express");
|
|
12
|
+
const agent_chat_manager_1 = require("./agent-chat-manager");
|
|
11
13
|
const providers_1 = require("../../../providers");
|
|
12
14
|
const provider_selection_1 = require("../../../provider-selection");
|
|
13
15
|
const agent_tier_1 = require("./agent-tier");
|
|
@@ -213,8 +215,72 @@ function createAgentChatRouter(deps) {
|
|
|
213
215
|
messages: (0, agent_input_store_1.decorateAgentInputMessages)(desktopDb, (0, agent_store_1.listAgentMessages)(desktopDb, conversation.id)),
|
|
214
216
|
pendingMessages: manager.pendingMessages(conversation.id),
|
|
215
217
|
live: manager.conversationLive(conversation.id),
|
|
218
|
+
session: manager.sessionState(conversation.id),
|
|
216
219
|
});
|
|
217
220
|
});
|
|
221
|
+
// ── Sub-agents of a mission's Core agent session (core-agent-sessions-host) ──
|
|
222
|
+
router.get('/conversations/:id/subagents', (req, res) => {
|
|
223
|
+
const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
|
|
224
|
+
if (!conversation) {
|
|
225
|
+
res.status(404).json({ error: 'Unknown conversation' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
res.json({ subagents: (0, agent_session_store_1.listSubagents)(desktopDb, conversation.id), session: manager.sessionState(conversation.id) });
|
|
229
|
+
});
|
|
230
|
+
router.get('/conversations/:id/subagents/:subagentId/events', (req, res) => {
|
|
231
|
+
const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
|
|
232
|
+
if (!conversation) {
|
|
233
|
+
res.status(404).json({ error: 'Unknown conversation' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const after = Number.parseInt(String(req.query.after ?? '0'), 10);
|
|
237
|
+
const limit = Number.parseInt(String(req.query.limit ?? '200'), 10);
|
|
238
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1000) {
|
|
239
|
+
res.status(400).json({ error: 'after must be ≥ 0 and limit between 1 and 1000' });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
res.json((0, agent_session_store_1.pageSubagentEvents)(desktopDb, conversation.id, String(req.params.subagentId), after, limit));
|
|
243
|
+
});
|
|
244
|
+
router.post('/conversations/:id/subagents/stop', async (req, res) => {
|
|
245
|
+
const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
|
|
246
|
+
if (!conversation) {
|
|
247
|
+
res.status(404).json({ error: 'Unknown conversation' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const ids = req.body?.subagentIds;
|
|
251
|
+
if (ids !== undefined && (!Array.isArray(ids) || ids.length > 500 || !ids.every((id) => typeof id === 'string' && id.length > 0 && id.length <= 200))) {
|
|
252
|
+
res.status(400).json({ error: 'subagentIds must be an array of ids' });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
res.json({ stopped: await manager.stopSubagents(conversation.id, ids) });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
res.status(502).json({ error: err instanceof Error ? err.message : 'Could not stop the sub-agents' });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
router.post('/conversations/:id/session/rebuild', async (req, res) => {
|
|
263
|
+
const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
|
|
264
|
+
if (!conversation) {
|
|
265
|
+
res.status(404).json({ error: 'Unknown conversation' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const result = await manager.rebuildSessionProjection(conversation.id);
|
|
270
|
+
if (!result) {
|
|
271
|
+
res.status(409).json({ error: 'This mission does not run in a Core session' });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
res.json(result);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
if (err instanceof agent_chat_manager_1.SessionRebuildBusyError) {
|
|
278
|
+
res.status(409).json({ error: err.message });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
res.status(502).json({ error: err instanceof Error ? err.message : 'Could not rebuild the session projection' });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
218
284
|
router.patch('/conversations/:id', (req, res) => {
|
|
219
285
|
const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
|
|
220
286
|
if (!conversation) {
|
|
@@ -289,6 +355,8 @@ function createAgentChatRouter(deps) {
|
|
|
289
355
|
router.delete('/conversations/:id', (req, res) => {
|
|
290
356
|
const id = String(req.params.id);
|
|
291
357
|
manager.abort(id);
|
|
358
|
+
// Must run before the row is deleted: it reads the conversation's scope.
|
|
359
|
+
void manager.closeCoreSession(id).catch((e) => console.error('[agent-chat] Core session close failed:', e));
|
|
292
360
|
(0, transient_children_1.killBackgroundProcessesForChat)(id);
|
|
293
361
|
(0, transient_children_1.purgeBackgroundProcessHistory)({ chatId: id });
|
|
294
362
|
(0, agent_store_1.deleteAgentConversation)(desktopDb, id);
|
|
@@ -546,13 +546,12 @@ the user's language.
|
|
|
546
546
|
a description following the template, at least one acceptance criterion, and
|
|
547
547
|
you have no outstanding question. Then — and only then — emit the complete
|
|
548
548
|
final \`spec-draft\` block one last time, state the one-line short summary you
|
|
549
|
-
will attach, and ask exactly one question: whether to create it.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
answers yes to that render. If they edit, update and re-render.
|
|
549
|
+
will attach, and ask exactly one question: whether to create it. The
|
|
550
|
+
Contract Layer is always added: never ask about it, never offer to skip it and
|
|
551
|
+
never present it as an option (the user does not want that question). Pass
|
|
552
|
+
\`contractRefine: false\` only when the user asked for no contract layer on
|
|
553
|
+
their own initiative. Do not call any persisting tool until the user answers
|
|
554
|
+
yes to that render. If they edit, update and re-render.
|
|
556
555
|
|
|
557
556
|
**Pausing.** There is no draft-ticket path for you (\`save_draft\` needs a real
|
|
558
557
|
app Explore conversation). If the user pauses, summarize the full draft state
|
|
@@ -569,8 +568,8 @@ summary later.
|
|
|
569
568
|
author — the same post-persist enrichment the app's Add Spec runs. After the
|
|
570
569
|
spec lands, one short background AI pass appends a \`## Contract Layer\`
|
|
571
570
|
section (naming contract, data shapes, state machine, invariants, file touch
|
|
572
|
-
list).
|
|
573
|
-
|
|
571
|
+
list). Never ask whether to add it. Pass \`contractRefine: false\` only when
|
|
572
|
+
the user asked, unprompted, for no contract layer or zero AI spend. It requires a structured-action provider (currently
|
|
574
573
|
Claude), respects the app-wide kill switch, and leaves the spec unenriched when
|
|
575
574
|
it cannot run.
|
|
576
575
|
- The enrichment is asynchronous: the commit returns immediately and the
|