mixdog 0.9.151 → 0.9.152

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.
Files changed (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,84 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { createGoalContinuation } from './goal-continuation.mjs';
5
+
6
+ const settleImmediate = () => new Promise((resolve) => setImmediate(resolve));
7
+
8
+ test('idle Goal schedules one hidden continuation and user input cancels it', async () => {
9
+ const goal = { id: 'goal-1', status: 'active', objective: 'Finish it' };
10
+ const state = { busy: false, commandBusy: false, sessionId: 'sess_goal', goal };
11
+ const pending = [];
12
+ let listener = null;
13
+ let archived = 0;
14
+ const controller = createGoalContinuation({
15
+ runtime: {
16
+ id: 'sess_goal',
17
+ goalStatus: () => goal,
18
+ goalContinuation: () => ({ run: true, reason: 'idle', goal, prompt: 'hidden continuation' }),
19
+ onGoalStatusChange: (next) => {
20
+ listener = next;
21
+ return () => { listener = null; };
22
+ },
23
+ archiveCompletedGoalOnUserInput: async () => { archived += 1; },
24
+ },
25
+ flags: { disposed: false, pendingSessionReset: false },
26
+ getState: () => state,
27
+ set: (patch) => Object.assign(state, patch),
28
+ getPending: () => pending,
29
+ enqueue: (text, options) => {
30
+ pending.push({ content: text, ...options });
31
+ return true;
32
+ },
33
+ });
34
+ try {
35
+ assert.equal(controller.scheduleGoalContinuation(), true);
36
+ controller.scheduleGoalContinuation();
37
+ await settleImmediate();
38
+ assert.equal(pending.length, 1);
39
+ assert.equal(pending[0].mode, 'goal-continuation');
40
+ assert.equal(pending[0].priority, 'later');
41
+ assert.equal(controller.shouldRunGoalContinuation(pending[0]), true);
42
+
43
+ controller.archiveCompletedGoalOnUserInput();
44
+ assert.equal(pending.length, 0);
45
+ await settleImmediate();
46
+ assert.equal(archived, 1);
47
+
48
+ listener?.({ sessionId: 'sess_goal', goal: { ...goal, status: 'paused' } });
49
+ await settleImmediate();
50
+ assert.equal(pending.length, 0);
51
+ } finally {
52
+ controller.disposeGoalContinuation();
53
+ }
54
+ });
55
+
56
+ test('busy or agent-parked Goal does not enqueue a continuation', async () => {
57
+ const goal = { id: 'goal-2', status: 'active' };
58
+ const state = { busy: true, commandBusy: false, sessionId: 'sess_goal_busy', goal };
59
+ const pending = [];
60
+ const controller = createGoalContinuation({
61
+ runtime: {
62
+ id: 'sess_goal_busy',
63
+ goalStatus: () => goal,
64
+ goalContinuation: () => ({ run: false, reason: 'agent-running', goal }),
65
+ onGoalStatusChange: () => () => {},
66
+ },
67
+ flags: { disposed: false, pendingSessionReset: false },
68
+ getState: () => state,
69
+ set: (patch) => Object.assign(state, patch),
70
+ getPending: () => pending,
71
+ enqueue: (text, options) => pending.push({ content: text, ...options }),
72
+ });
73
+ try {
74
+ controller.scheduleGoalContinuation();
75
+ await settleImmediate();
76
+ assert.equal(pending.length, 0);
77
+ state.busy = false;
78
+ controller.onGoalTurnSettled('done');
79
+ await settleImmediate();
80
+ assert.equal(pending.length, 0);
81
+ } finally {
82
+ controller.disposeGoalContinuation();
83
+ }
84
+ });
@@ -36,7 +36,10 @@ export function defaultQueuePriority(mode) {
36
36
 
37
37
  export function isQueuedEntryEditable(entry) {
38
38
  const mode = entry?.mode || 'prompt';
39
- return mode !== 'task-notification' && mode !== 'pending-resume' && entry?.isMeta !== true;
39
+ return mode !== 'task-notification'
40
+ && mode !== 'pending-resume'
41
+ && mode !== 'goal-continuation'
42
+ && entry?.isMeta !== true;
40
43
  }
41
44
 
42
45
  export function isQueuedEntryVisible(entry) {
@@ -44,7 +47,7 @@ export function isQueuedEntryVisible(entry) {
44
47
  // task completions stay in the internal pending queue, but should never look
45
48
  // like commands typed by the user while they wait to be drained.
46
49
  const mode = entry?.mode || 'prompt';
47
- if (mode === 'pending-resume') return false;
50
+ if (mode === 'pending-resume' || mode === 'goal-continuation') return false;
48
51
  return isQueuedEntryEditable(entry);
49
52
  }
50
53
 
@@ -0,0 +1,108 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { createSessionApi } from './session-api.mjs';
5
+ import {
6
+ SESSION_CONFIGURE_ACTIONS,
7
+ SESSION_READ_ACTIONS,
8
+ } from '../../standalone/session-protocol.mjs';
9
+
10
+ // The daemon resolves every session action by NAME on the object
11
+ // createLocalSessionRuntime returns — this surface. An action that only exists
12
+ // on the runtime beneath it passes the protocol allowlist and then dies in the
13
+ // session runtime worker as "session action <name> is unavailable" (desktop
14
+ // /inherit shipped exactly that way).
15
+ function stubBag(overrides = {}) {
16
+ const defaults = {
17
+ flags: {},
18
+ autoClearState: {},
19
+ pending: [],
20
+ listeners: new Set(),
21
+ };
22
+ return new Proxy({}, {
23
+ get: (_target, key) => {
24
+ if (key in overrides) return overrides[key];
25
+ if (key in defaults) return defaults[key];
26
+ return () => ({});
27
+ },
28
+ has: () => true,
29
+ });
30
+ }
31
+
32
+ test('every advertised session action exists on the session surface', () => {
33
+ const api = createSessionApi(stubBag());
34
+ const missing = [...SESSION_READ_ACTIONS, ...SESSION_CONFIGURE_ACTIONS]
35
+ .filter((action) => typeof api[action] !== 'function');
36
+ assert.deepEqual(missing, []);
37
+ });
38
+
39
+ test('extension editor actions are advertised and forwarded', async () => {
40
+ const calls = [];
41
+ let state = { commandBusy: false, stats: {} };
42
+ const api = createSessionApi(stubBag({
43
+ runtime: {
44
+ getMcpServerConfig: (name) => {
45
+ calls.push(['getMcpServerConfig', name]);
46
+ return { name };
47
+ },
48
+ saveMcpServer: async (input) => {
49
+ calls.push(['saveMcpServer', input]);
50
+ return { name: input.name };
51
+ },
52
+ saveSkill: async (input) => {
53
+ calls.push(['saveSkill', input]);
54
+ return { skill: { name: input.name } };
55
+ },
56
+ },
57
+ getState: () => state,
58
+ set: (patch) => { state = { ...state, ...patch }; },
59
+ routeState: () => ({}),
60
+ }));
61
+
62
+ assert.ok(SESSION_READ_ACTIONS.includes('getMcpServerConfig'));
63
+ assert.ok(SESSION_CONFIGURE_ACTIONS.includes('saveMcpServer'));
64
+ assert.ok(SESSION_CONFIGURE_ACTIONS.includes('saveSkill'));
65
+ assert.deepEqual(api.getMcpServerConfig('UnityMCP'), { name: 'UnityMCP' });
66
+ await api.saveMcpServer({ name: 'UnityMCP' });
67
+ await api.saveSkill({ name: 'unity-helper' });
68
+ assert.deepEqual(calls, [
69
+ ['getMcpServerConfig', 'UnityMCP'],
70
+ ['saveMcpServer', { name: 'UnityMCP' }],
71
+ ['saveSkill', { name: 'unity-helper' }],
72
+ ]);
73
+ });
74
+
75
+ test('inheritFrom carries the conversation and rebuilds the heir transcript', async () => {
76
+ const carried = [
77
+ { role: 'system', content: 'target system prompt' },
78
+ { role: 'user', content: 'first question' },
79
+ { role: 'assistant', content: 'first answer' },
80
+ ];
81
+ let state = { sessionId: 'sess_heir', stats: {}, items: [] };
82
+ const calls = [];
83
+ const api = createSessionApi(stubBag({
84
+ runtime: {
85
+ inheritFrom: (sourceSessionId) => {
86
+ calls.push(sourceSessionId);
87
+ return { sessionId: 'sess_heir', sourceSessionId, messages: 2 };
88
+ },
89
+ readModelMessages: () => ({ messageCount: carried.length, messages: carried }),
90
+ },
91
+ getState: () => state,
92
+ set: (patch) => { state = { ...state, ...patch }; },
93
+ replaceItems: (items) => items,
94
+ routeState: () => ({}),
95
+ }));
96
+
97
+ const result = await api.inheritFrom('sess_source');
98
+
99
+ assert.deepEqual(calls, ['sess_source']);
100
+ assert.equal(result.messages, 2);
101
+ assert.equal(state.sessionId, 'sess_heir');
102
+ // Only the conversation travels: the target's own system block never becomes
103
+ // a transcript item.
104
+ assert.deepEqual(
105
+ state.items.map((item) => [item.kind, item.text]),
106
+ [['user', 'first question'], ['assistant', 'first answer']],
107
+ );
108
+ });
@@ -437,9 +437,40 @@ export function sessionContextSnapshotProjection(session, contextStatus) {
437
437
 
438
438
  export function createSessionApiB(bag) {
439
439
  const {
440
- runtime, nextId, flags, lifecycle, listeners, getState, set, flushEmitImmediate, disposeEmit, replaceItems, pushNotice, removeNotice, setProgressHint, clearToastTimers, disposeTranscriptSpill, routeState, syncContextStats, finishToolApproval, denyAllToolApprovals, restoreLeadSteeringFromDisk, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext,
440
+ runtime, nextId, flags, lifecycle, listeners, getState, set, flushEmitImmediate, disposeEmit, replaceItems, pushNotice, removeNotice, setProgressHint, clearToastTimers, disposeTranscriptSpill, disposeGoalContinuation, routeState, syncContextStats, finishToolApproval, denyAllToolApprovals, restoreLeadSteeringFromDisk, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext,
441
441
  } = bag;
442
442
  const oauthFlows = createSessionOAuthFlowRegistry();
443
+ /**
444
+ * Session inheritance as ONE addressable session action. THIS session is the
445
+ * already-created heir, so only the carry step runs here — the desktop
446
+ * creates and routes the new session first, then calls this by name. The
447
+ * daemon resolves actions on THIS surface, so `inheritFrom` has to live here
448
+ * and not only on the runtime beneath it.
449
+ */
450
+ const inheritFrom = async (sourceSessionId) => {
451
+ const result = await runtime.inheritFrom(sourceSessionId);
452
+ const sessionId = String(result?.sessionId || runtime.sessionId || getState().sessionId || '');
453
+ // Only model messages travel with the conversation. Rebuild the visible
454
+ // transcript from them right here so the heir opens showing the carried
455
+ // conversation; without it the view stays blank until a cold reopen
456
+ // resumes the session from disk and restores the same items.
457
+ const carried = runtime.readModelMessages?.(0)?.messages;
458
+ const items = restoreTranscriptItems(Array.isArray(carried) ? carried : [], { sessionId });
459
+ resetStatsAndSyncContext();
460
+ set({
461
+ sessionId,
462
+ items: replaceItems(items),
463
+ toasts: [],
464
+ queued: [],
465
+ thinking: null,
466
+ spinner: null,
467
+ lastTurn: null,
468
+ ...routeState(),
469
+ stats: { ...getState().stats },
470
+ });
471
+ flushEmitImmediate();
472
+ return result;
473
+ };
443
474
  return {
444
475
  resolveToolApproval: (id, decision = {}) => {
445
476
  const approved = decision === true || decision?.approved === true;
@@ -639,17 +670,17 @@ export function createSessionApiB(bag) {
639
670
  getTurnReviewDiff: async (options = {}) => {
640
671
  return (await runtime.getTurnReviewDiff?.(options)) ?? { supported: false, files: [], patch: '' };
641
672
  },
642
- revertTurnReview: async () => {
673
+ revertTurnReview: async (checkpointId) => {
643
674
  if (typeof runtime.revertTurnReview !== 'function') {
644
675
  throw new Error('Turn review revert is unavailable');
645
676
  }
646
- return await runtime.revertTurnReview();
677
+ return await runtime.revertTurnReview(checkpointId);
647
678
  },
648
- revertTurnReviewFile: async (file) => {
679
+ revertTurnReviewFile: async (file, checkpointId) => {
649
680
  if (typeof runtime.revertTurnReviewFile !== 'function') {
650
681
  throw new Error('Turn review revert is unavailable');
651
682
  }
652
- return await runtime.revertTurnReviewFile(file);
683
+ return await runtime.revertTurnReviewFile(file, checkpointId);
653
684
  },
654
685
  getOnboardingStatus: () => {
655
686
  return runtime.getOnboardingStatus?.() || { completed: true, workflowRoutes: {} };
@@ -915,6 +946,7 @@ export function createSessionApiB(bag) {
915
946
  * conversation into it. The source session file is left as it is, so the
916
947
  * two transcripts share a prefix and then diverge.
917
948
  */
949
+ inheritFrom,
918
950
  inheritSession: async () => {
919
951
  if (getState().commandBusy) return false;
920
952
  const sourceId = getState().sessionId || null;
@@ -922,9 +954,7 @@ export function createSessionApiB(bag) {
922
954
  set({ commandBusy: true });
923
955
  try {
924
956
  await runtime.newSession();
925
- const result = await runtime.inheritFrom(sourceId);
926
- set({ sessionId: runtime.sessionId ?? getState().sessionId });
927
- return result;
957
+ return await inheritFrom(sourceId);
928
958
  } finally {
929
959
  set({ commandBusy: false });
930
960
  }
@@ -1071,6 +1101,7 @@ export function createSessionApiB(bag) {
1071
1101
  try { runtime.clearSessionPresence?.(); } catch { /* best-effort */ }
1072
1102
  clearToastTimers();
1073
1103
  disposeTranscriptSpill?.();
1104
+ disposeGoalContinuation?.();
1074
1105
  try { clearInterval(lifecycle.runtimePulseTimer); } catch {}
1075
1106
  try { lifecycle.unsubscribeRuntimeNotifications?.(); } catch {}
1076
1107
  lifecycle.unsubscribeRuntimeNotifications = null;
@@ -85,6 +85,9 @@ export function createSessionApiA(bag) {
85
85
  // turn or the background task. The returned running snapshot creates the
86
86
  // normal post-tool boundary, where this queued prompt is injected.
87
87
  if (accepted !== false) {
88
+ if ((intake.queueOptions.mode || 'prompt') === 'prompt') {
89
+ bag.archiveCompletedGoalOnUserInput?.();
90
+ }
88
91
  try { runtime.interruptTaskWait?.('user-message'); } catch {}
89
92
  }
90
93
  return accepted;
@@ -381,6 +384,18 @@ export function createSessionApiA(bag) {
381
384
  set({ commandBusy: false });
382
385
  }
383
386
  },
387
+ // Background-task control (list/read/monitor/cancel) is a CONTROL surface,
388
+ // not a conversation turn: the desktop stop button fires it WHILE a turn
389
+ // runs, so it neither waits on commandBusy nor leaves a transcript card.
390
+ // It must exist here because the daemon addresses actions on this surface.
391
+ taskControl: (args = {}) => {
392
+ return runtime.taskControl?.(args) ?? null;
393
+ },
394
+ goalControl: async (args = {}) => {
395
+ const result = await runtime.goalControl?.(args);
396
+ set({ goal: runtime.goalStatus?.() || result?.goal || null });
397
+ return result;
398
+ },
384
399
  toolsStatus: (query = '') => {
385
400
  return runtime.toolsStatus?.(query) || { mode: getState().toolMode, count: 0, activeCount: 0, tools: [] };
386
401
  },
@@ -428,6 +443,9 @@ export function createSessionApiA(bag) {
428
443
  mcpStatus: () => {
429
444
  return runtime.mcpStatus?.() || { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
430
445
  },
446
+ getMcpServerConfig: (name) => {
447
+ return runtime.getMcpServerConfig?.(name);
448
+ },
431
449
  reconnectMcp: async () => {
432
450
  if (getState().commandBusy) return null;
433
451
  set({ commandBusy: true });
@@ -457,6 +475,19 @@ export function createSessionApiA(bag) {
457
475
  set({ commandBusy: false });
458
476
  }
459
477
  },
478
+ saveMcpServer: async (input) => {
479
+ if (getState().commandBusy) return null;
480
+ set({ commandBusy: true });
481
+ try {
482
+ const result = await runtime.saveMcpServer?.(input);
483
+ resetStatsAndSyncContext();
484
+ set({ ...routeState(), stats: { ...getState().stats } });
485
+ pushNotice(`mcp saved: ${result?.name || input?.name || 'server'}`, 'info');
486
+ return result;
487
+ } finally {
488
+ set({ commandBusy: false });
489
+ }
490
+ },
460
491
  removeMcpServer: async (name) => {
461
492
  if (getState().commandBusy) return null;
462
493
  set({ commandBusy: true });
@@ -514,6 +545,19 @@ export function createSessionApiA(bag) {
514
545
  set({ commandBusy: false });
515
546
  }
516
547
  },
548
+ saveSkill: async (input) => {
549
+ if (getState().commandBusy) return null;
550
+ set({ commandBusy: true });
551
+ try {
552
+ const result = await runtime.saveSkill?.(input);
553
+ resetStatsAndSyncContext();
554
+ set({ ...routeState(), stats: { ...getState().stats } });
555
+ pushNotice(`skill saved: ${result?.skill?.name || input?.name || 'skill'}`, 'info');
556
+ return result;
557
+ } finally {
558
+ set({ commandBusy: false });
559
+ }
560
+ },
517
561
  reloadSkills: async () => {
518
562
  if (getState().commandBusy) return null;
519
563
  set({ commandBusy: true });
@@ -569,6 +613,20 @@ export function createSessionApiA(bag) {
569
613
  set({ commandBusy: false });
570
614
  }
571
615
  },
616
+ setPluginEnabled: async (plugin, enabled) => {
617
+ if (getState().commandBusy) return null;
618
+ set({ commandBusy: true });
619
+ try {
620
+ const result = await runtime.setPluginEnabled?.(plugin, enabled);
621
+ resetStatsAndSyncContext();
622
+ set({ ...routeState(), stats: { ...getState().stats } });
623
+ pushNotice(`plugin ${enabled === false ? 'disabled' : 'enabled'}: ${
624
+ result?.plugin?.title || result?.plugin?.name || plugin?.name || plugin}`, 'info');
625
+ return result;
626
+ } finally {
627
+ set({ commandBusy: false });
628
+ }
629
+ },
572
630
  removePlugin: async (plugin) => {
573
631
  if (getState().commandBusy) return null;
574
632
  set({ commandBusy: true });
@@ -88,6 +88,8 @@ export function createSessionFlow(bag) {
88
88
  : [],
89
89
  steeringPersistId: options.steeringPersistId || null,
90
90
  steeringPersistRestored: options.steeringPersistRestored === true,
91
+ isMeta: options.isMeta === true,
92
+ goalId: options.goalId || null,
91
93
  };
92
94
  }
93
95
 
@@ -215,6 +217,10 @@ export function createSessionFlow(bag) {
215
217
  });
216
218
  firstBatch = false;
217
219
  if (batch.length === 0) break;
220
+ if (batch[0]?.mode === 'goal-continuation'
221
+ && bag.shouldRunGoalContinuation?.(batch[0]) !== true) {
222
+ continue;
223
+ }
218
224
  tuiDebug(`busy-queue drain batch=${batch.length} remaining=${pending.length}`);
219
225
  const ids = new Set(batch.map((e) => e.id));
220
226
  const merged = mergePromptContents(batch);
@@ -1384,6 +1384,7 @@ export function createRunTurn(bag) {
1384
1384
  flushEmit?.();
1385
1385
  _publishedThinkingActive = false; // turn teardown cleared getState().thinking
1386
1386
  const finalStatus = cancelled ? 'cancelled' : (failed ? 'failed' : 'done');
1387
+ try { bag.onGoalTurnSettled?.(finalStatus); } catch {}
1387
1388
  tuiDebug(`runTurn end turn=${turnIndex} status=${finalStatus} elapsedMs=${Date.now() - startedAt} pending=${pending.length}`);
1388
1389
  return finalStatus;
1389
1390
  }
@@ -31,6 +31,7 @@ import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
31
31
  import { resetAllStreamingMarkdownStablePrefixes } from './markdown/streaming-markdown.mjs';
32
32
  import { bootProfile } from './session/boot-profile.mjs';
33
33
  import { createSessionStats, applyUsageDelta } from './session/session-stats.mjs';
34
+ import { createGoalContinuation } from './session/goal-continuation.mjs';
34
35
  import {
35
36
  pickVerb,
36
37
  pickDoneVerb,
@@ -896,6 +897,14 @@ export async function createLocalSessionRuntime({
896
897
  kickExecutionPendingResume, flushDeferredExecutionPendingResumeKick, scheduleExecutionPendingResumeKick, discardExecutionPendingResume, updateAgentJobCard, subscribeRuntimeNotifications,
897
898
  });
898
899
  Object.assign(bag, createSessionFlow(bag));
900
+ Object.assign(bag, createGoalContinuation({
901
+ runtime,
902
+ flags,
903
+ getState: () => state,
904
+ set,
905
+ getPending: () => pending,
906
+ enqueue: (...args) => bag.enqueue(...args),
907
+ }));
899
908
  bag.runTurn = createRunTurn(bag);
900
909
  const api = createSessionApi(bag);
901
910
  // Cross-surface share: presence + the durable pending spool remain the
@@ -1069,6 +1078,9 @@ export async function createLocalSessionRuntime({
1069
1078
  const base = api[method].bind(api);
1070
1079
  api[method] = async (...args) => {
1071
1080
  const result = await base(...args);
1081
+ bag.cancelQueuedGoalContinuations?.();
1082
+ bag.refreshGoalState?.();
1083
+ bag.scheduleGoalContinuation?.();
1072
1084
  reconcileLiveShareNow();
1073
1085
  if (method === 'resume' && result === true && state.sessionRemoteAttached) {
1074
1086
  const id = String(state.sessionId || '');
@@ -1165,6 +1177,8 @@ export async function createLocalSessionRuntime({
1165
1177
  } catch { /* best-effort */ }
1166
1178
  }, 3000);
1167
1179
  remoteAttachTimer.unref?.();
1168
- void bag.restoreLeadSteeringFromDisk();
1180
+ void Promise.resolve(bag.restoreLeadSteeringFromDisk())
1181
+ .catch(() => {})
1182
+ .finally(() => bag.scheduleGoalContinuation?.());
1169
1183
  return api;
1170
1184
  }