mixdog 1.2.0 → 1.2.1

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 (201) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -0
  4. package/src/defaults/skills/pptx/SKILL.md +6 -6
  5. package/src/defaults/skills/pptx/references/review.md +6 -4
  6. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  7. package/src/defaults/skills/setup/references/actions.md +13 -3
  8. package/src/defaults/skills/visualize/SKILL.md +12 -9
  9. package/src/rules/shared/60-verification.md +3 -1
  10. package/src/rules/shared/70-delivery.md +4 -2
  11. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  12. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  13. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  14. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  15. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  16. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
  17. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
  20. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  21. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
  25. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  27. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  28. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  29. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  31. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  33. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  35. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  36. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  37. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  38. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
  44. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  46. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  48. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  49. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
  50. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  53. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  54. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  55. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  56. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  57. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  58. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  59. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  60. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  61. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  62. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  63. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  64. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  66. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  68. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  71. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  77. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  78. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  79. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  80. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  82. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
  84. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  85. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  86. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  87. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
  89. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  92. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  99. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  102. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  104. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  106. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  107. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  108. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  110. package/src/runtime/attachments/store.d.mts +3 -0
  111. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  112. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  113. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  114. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  115. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  116. package/src/runtime/memory/lib/memory.mjs +1 -1
  117. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  118. package/src/runtime/shared/atomic-file.mjs +25 -4
  119. package/src/runtime/shared/extension-scopes.mjs +5 -3
  120. package/src/runtime/shared/file-lock.mjs +37 -11
  121. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  122. package/src/runtime/shared/llm/model-catalog.mjs +0 -12
  123. package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
  124. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  125. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  126. package/src/runtime/shared/pristine-execution.mjs +28 -4
  127. package/src/runtime/shared/provider-accounts.mjs +124 -20
  128. package/src/runtime/shared/provider-api-key.mjs +12 -0
  129. package/src/runtime/shared/shell-display.mjs +9 -3
  130. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  131. package/src/runtime/shared/skill-mentions.mjs +133 -0
  132. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  133. package/src/runtime/shared/tool-primitives.mjs +35 -2
  134. package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
  135. package/src/runtime/shared/worker-requests.mjs +15 -2
  136. package/src/session-runtime/automation-agents.mjs +8 -2
  137. package/src/session-runtime/boot/apis.mjs +55 -0
  138. package/src/session-runtime/boot/config.mjs +5 -2
  139. package/src/session-runtime/boot/facade.mjs +1 -0
  140. package/src/session-runtime/boot/infrastructure.mjs +29 -18
  141. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  142. package/src/session-runtime/conversation-import/service.mjs +369 -0
  143. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  144. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  145. package/src/session-runtime/mcp-glue/connect.mjs +57 -10
  146. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  147. package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
  148. package/src/session-runtime/mcp-glue.mjs +22 -2
  149. package/src/session-runtime/new-session-config.mjs +3 -2
  150. package/src/session-runtime/notification-bus.mjs +9 -2
  151. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  152. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  153. package/src/session-runtime/provider-readiness.mjs +16 -10
  154. package/src/session-runtime/resource-extension-sync.mjs +20 -5
  155. package/src/session-runtime/resource-mcp-api.mjs +101 -77
  156. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  157. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  158. package/src/session-runtime/resource-skills-api.mjs +23 -2
  159. package/src/session-runtime/schedule-session-run.mjs +1 -1
  160. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  161. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  162. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  163. package/src/session-runtime/services/provider-admin.mjs +64 -18
  164. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  165. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  166. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  167. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  168. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  169. package/src/session-runtime/skills-api.mjs +7 -3
  170. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  171. package/src/session-runtime/webhook-session-run.mjs +16 -4
  172. package/src/skill-script-entry.mjs +36 -0
  173. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  174. package/src/standalone/daemon.mjs +41 -4
  175. package/src/standalone/session-protocol.mjs +5 -0
  176. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  177. package/src/tui/App.jsx +9 -1
  178. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  179. package/src/tui/app/app-view.jsx +6 -0
  180. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  181. package/src/tui/app/model-options.mjs +2 -0
  182. package/src/tui/app/skill-mention-palette.mjs +88 -0
  183. package/src/tui/app/slash-palette.mjs +23 -7
  184. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  185. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  186. package/src/tui/app/use-prompt-state.mjs +5 -0
  187. package/src/tui/components/Message.jsx +22 -1
  188. package/src/tui/components/PromptInput.jsx +21 -3
  189. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  190. package/src/tui/components/TranscriptItem.jsx +9 -1
  191. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  192. package/src/tui/components/user-message-skills.mjs +38 -0
  193. package/src/tui/dist/index.mjs +507 -171
  194. package/src/tui/session/queue-helpers.mjs +7 -2
  195. package/src/tui/session/session-api/extensions.mjs +4 -2
  196. package/src/tui/session/session-api/settings.mjs +7 -0
  197. package/src/tui/session/session-flow/drain.mjs +14 -4
  198. package/src/tui/session/tui-steering-persist.mjs +25 -9
  199. package/src/tui/session/turn.mjs +17 -0
  200. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  201. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -175,7 +175,9 @@ export function mergePastedImages(entries) {
175
175
 
176
176
  // Byte-free image metadata for the user transcript item. Prompt content carries
177
177
  // only a content-addressed attachment ref after daemon intake; transcript items
178
- // and desktop snapshots carry just name/mime/size.
178
+ // and desktop snapshots carry name/mime/size plus, for attachment-reference
179
+ // parts, the `attachmentRef` so the desktop can rebuild thumbnails from the
180
+ // stored original.
179
181
  export function promptContentImageMeta(content, pastedImages) {
180
182
  const parts = Array.isArray(content) ? content.filter((part) => part?.type === 'image') : [];
181
183
  if (parts.length === 0) return null;
@@ -190,6 +192,7 @@ export function promptContentImageMeta(content, pastedImages) {
190
192
  name: String(meta?.filename || `Image ${index + 1}`),
191
193
  mimeType: String(part.mimeType || part.mediaType || meta?.mediaType || 'image/png'),
192
194
  bytes: Number(part.sizeBytes) || data.length,
195
+ ...(isAttachmentReference(part) ? { attachmentRef: part.attachmentRef } : {}),
193
196
  };
194
197
  });
195
198
  }
@@ -202,6 +205,8 @@ export function callCommitCallbacks(entries) {
202
205
  for (const entry of entries || []) {
203
206
  try {
204
207
  entry?.onCommitted?.();
205
- } catch {}
208
+ } catch {
209
+ // A throwing commit callback must not block the remaining entries.
210
+ }
206
211
  }
207
212
  }
@@ -83,8 +83,10 @@ export function createSessionExtensionsApi(bag) {
83
83
  }
84
84
  return await runtime.setExtensionScope(kind, name, projects);
85
85
  },
86
- skillsStatus: () => {
87
- return runtime.skillsStatus?.() || { cwd: getState().cwd, count: 0, skills: [] };
86
+ // `options.cwd`: list and scope skills for that folder instead of this
87
+ // session's (the desktop resolves a new-task draft's Project to it).
88
+ skillsStatus: (options) => {
89
+ return runtime.skillsStatus?.(options) || { cwd: getState().cwd, count: 0, skills: [] };
88
90
  },
89
91
  skillContent: (name) => {
90
92
  return runtime.skillContent?.(name);
@@ -154,6 +154,13 @@ export function createSessionSettingsApi(bag) {
154
154
  inspectHuggingFaceModel: (options) => runtime.inspectHuggingFaceModel(options),
155
155
  registerHuggingFaceModel: (previewId, licenseAccepted) =>
156
156
  runtime.registerHuggingFaceModel(previewId, licenseAccepted),
157
+ // Conversation import (advertised session actions; one object argument).
158
+ // Background jobs, so never behind commandBusy.
159
+ listImportableConversations: (params) => runtime.listImportableConversations(params),
160
+ getConversationImportJobs: (params) => runtime.getConversationImportJobs(params),
161
+ registerConversationImportFile: (params) => runtime.registerConversationImportFile(params),
162
+ startConversationImport: (params) => runtime.startConversationImport(params),
163
+ cancelConversationImport: (params) => runtime.cancelConversationImport(params),
157
164
  getChannelSettings: (options = {}) => {
158
165
  return (
159
166
  runtime.getChannelSettings?.(options) || {
@@ -65,7 +65,19 @@ function renderBatchItems(batch, pushUserOrSyntheticItem) {
65
65
  }
66
66
  }
67
67
 
68
- function turnOptionsForBatch(batch, steering) {
68
+ const COMPLETION_WAKE_MODES = new Set(['task-notification', 'pending-resume']);
69
+
70
+ // Trusted provenance of a drained batch: Goal turns keep their own source; a
71
+ // batch made only of completion wakes (task notifications, pending resumes)
72
+ // is the runtime reporting, carried as a task notification so the runtime
73
+ // stores it as one and never treats its text as a user skill selection.
74
+ function batchPromptSource(batch) {
75
+ if (isGoalQueuedEntry(batch[0])) return batch[0].mode;
76
+ if (batch.length > 0 && batch.every((entry) => COMPLETION_WAKE_MODES.has(entry?.mode))) return 'task-notification';
77
+ return undefined;
78
+ }
79
+
80
+ export function turnOptionsForBatch(batch, steering) {
69
81
  const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
70
82
  // A completion resume is owned by the completion that woke it. Esc
71
83
  // consumes that ownership; unlike ordinary notifications it must never
@@ -75,7 +87,7 @@ function turnOptionsForBatch(batch, steering) {
75
87
  (entry) => entry?.abortDiscardOnAbort === true || entry?.mode === 'pending-resume'
76
88
  );
77
89
  return {
78
- promptSource: isGoalQueuedEntry(batch[0]) ? batch[0].mode : undefined,
90
+ promptSource: batchPromptSource(batch),
79
91
  retryFailedTurn: batch[0]?.retryFailedTurn === true,
80
92
  displayText: batch
81
93
  .map((entry) => entry.text)
@@ -114,8 +126,6 @@ function turnOptionsForBatch(batch, steering) {
114
126
  };
115
127
  }
116
128
 
117
- const COMPLETION_WAKE_MODES = new Set(['task-notification', 'pending-resume']);
118
-
119
129
  export function createDrainLoop(
120
130
  bag,
121
131
  { queue, steering, submissions, flushDeferredClearedSessionUi, autoClearBeforeWake }
@@ -15,11 +15,19 @@ const STALE_STEERING_RESTORE_TTL_MS = 30 * 60 * 1000;
15
15
  // Cross-process mutual exclusion still comes from the shared lock protocol;
16
16
  // this chain only orders same-process operations that were previously
17
17
  // naturally ordered by the synchronous call site.
18
- let _persistChain = Promise.resolve();
19
- function _serialize(task) {
20
- const run = _persistChain.then(task, task);
18
+ // One chain per session key so a slow write for one session never delays
19
+ // another; an entry is removed once its chain is idle.
20
+ const _persistChains = new Map();
21
+ function _serialize(key, task) {
22
+ const prev = _persistChains.get(key) || Promise.resolve();
23
+ const run = prev.then(task, task);
21
24
  // Never let a rejection poison the chain; each op logs its own error.
22
- _persistChain = run.catch(() => {});
25
+ const tail = run
26
+ .catch(() => {})
27
+ .then(() => {
28
+ if (_persistChains.get(key) === tail) _persistChains.delete(key);
29
+ });
30
+ _persistChains.set(key, tail);
23
31
  return run;
24
32
  }
25
33
 
@@ -159,7 +167,7 @@ export function appendTuiSteeringPersist(leadSessionId, entry) {
159
167
  ...(submissionId ? { submissionId } : {}),
160
168
  ...(Number.isFinite(submittedAt) && submittedAt > 0 ? { submittedAt } : {}),
161
169
  };
162
- return _serialize(async () => {
170
+ return _serialize(key, async () => {
163
171
  try {
164
172
  await updatePendingShard(key, (raw) => {
165
173
  const next = normalizePendingStore(raw);
@@ -184,7 +192,7 @@ export function dropTuiSteeringPersist(leadSessionId, entries) {
184
192
  if (!key) return Promise.resolve();
185
193
  const batch = Array.isArray(entries) ? entries : [];
186
194
  if (batch.length === 0) return Promise.resolve();
187
- return _serialize(async () => {
195
+ return _serialize(key, async () => {
188
196
  try {
189
197
  await updatePendingShard(key, (raw) => {
190
198
  const next = normalizePendingStore(raw);
@@ -207,8 +215,16 @@ export function dropTuiSteeringPersist(leadSessionId, entries) {
207
215
  });
208
216
  }
209
217
 
210
- export function flushTuiSteeringPersist() {
211
- return _persistChain.catch(() => {});
218
+ export async function flushTuiSteeringPersist() {
219
+ // Loop: chains started while waiting are awaited too.
220
+ while (_persistChains.size > 0) {
221
+ await Promise.all([..._persistChains.values()]);
222
+ }
223
+ }
224
+
225
+ // Test hook: number of live per-key chains.
226
+ export function _steeringPersistChainCount() {
227
+ return _persistChains.size;
212
228
  }
213
229
 
214
230
  function drainedRowToRestore(row) {
@@ -233,7 +249,7 @@ function drainedRowToRestore(row) {
233
249
  export function drainTuiSteeringPersist(leadSessionId) {
234
250
  const key = tuiSteeringSessionKey(leadSessionId);
235
251
  if (!key) return Promise.resolve([]);
236
- return _serialize(async () => {
252
+ return _serialize(key, async () => {
237
253
  let drained = [];
238
254
  let droppedStale = 0;
239
255
  try {
@@ -322,9 +322,26 @@ function applyTurnUsageDelta({ turn, getState, set, syncContextStats }, delta) {
322
322
  set({ stats: { ...getState().stats }, ...(spinner ? { spinner: { ...spinner, inputTokens, outputTokens } } : {}) });
323
323
  }
324
324
 
325
+ // The runtime reports the skills a user turn actually loaded (`Skill:` header
326
+ // or inline `$name`); the prompt row carries them as `appliedSkills`, the same
327
+ // field a restored row reads from the persisted transcript metadata.
328
+ function applySkillsToPromptRow({ turn, getState, patchItem }, names, detail = {}) {
329
+ const appliedSkills = (Array.isArray(names) ? names : []).filter((name) => typeof name === 'string' && name);
330
+ if (!appliedSkills.length) return;
331
+ const messageId = typeof detail?.messageId === 'string' ? detail.messageId : '';
332
+ const submitted = turn.submittedIds[0];
333
+ const row = getState().items.findLast(
334
+ (item) => item?.kind === 'user' && (messageId ? item.messageId === messageId : item.id === submitted)
335
+ );
336
+ if (row) patchItem(row.id, { appliedSkills });
337
+ }
338
+
325
339
  function turnHandlers(ctx) {
326
340
  const { isCurrentTurn, drainPendingSteering } = ctx;
327
341
  return {
342
+ onSkillsApplied: (names, detail) => {
343
+ if (isCurrentTurn()) applySkillsToPromptRow(ctx, names, detail);
344
+ },
328
345
  drainSteering: (_sessionId, drainOptions) =>
329
346
  isCurrentTurn() ? drainPendingSteering({ ...drainOptions, turnEpoch: ctx.turn.epoch }) : [],
330
347
  onStreamDelta: () => {},
@@ -1,162 +0,0 @@
1
- // Pure pre-turn extraction of the previous human request and the assistant
2
- // text around it, from the session history as it stands BEFORE the current
3
- // turn is committed (the boundary resolveTurnAutoEffort runs at). Nothing here
4
- // reads the current prompt's outcome; nothing is mutated; no text is matched
5
- // against word lists. Ownership comes only from typed provenance that the
6
- // runtime already writes (meta.runtimeUserContext, meta.synthetic, meta.source,
7
- // the compaction/skill/reminder predicates in compact/messages.mjs).
8
- //
9
- // Result of previousHumanContext(messages, { retryPrompt }):
10
- // {
11
- // version: 1,
12
- // boundary: number, // messages[0..boundary) were considered
13
- // retryTrimmedIndex: number, // index of the unanswered resubmitted prompt
14
- // // dropped from the view, or -1
15
- // prevHuman: { text, index, source, runtimeContext } | null,
16
- // // latest actual human message before the boundary. `text` is the
17
- // // runtime-context-stripped, trimmed text (untruncated). `source` is
18
- // // meta.source or null ('steering' = a human interjection).
19
- // // `runtimeContext` is 'stripped' | 'none'.
20
- // prevHumanMissing: 'empty-history' | 'no-human-request' | null,
21
- // canonicalReply: { text, index } | null,
22
- // // exactly what the shipped rp `prev` is: the latest non-empty
23
- // // assistant message before the boundary, whatever turn it is from.
24
- // pairedReply: { text, index } | null,
25
- // // latest non-empty assistant text inside prevHuman's own turn span
26
- // // (after prevHuman up to the next human message / boundary). Never
27
- // // falls back to another turn: a tool-only or unanswered turn gives
28
- // // null with pairedMissing set.
29
- // pairedMissing: 'no-human-request' | 'no-assistant-text-in-turn' | null,
30
- // replyIsPaired: boolean, // canonicalReply and pairedReply are the same message
31
- // skipped: [{ index, role, reason }], // user messages passed over after
32
- // // prevHuman's search began (newest first); reasons: 'summary',
33
- // // 'protected-context', 'skill-body', 'synthetic-flag',
34
- // // 'typed-source', 'runtime-owned', 'empty'.
35
- // }
36
- //
37
- // Policy decisions (PREVIOUS_HUMAN_POLICY):
38
- // synthetic meta.synthetic === true, or a source the runtime stores as
39
- // "not the user speaking" (task-notification, skill-context),
40
- // or anything isActualUserInstructionMessage rejects
41
- // (goal-continuation/closeout, compact-*, *-recovery, control
42
- // lines), is skipped, not treated as a request.
43
- // steering meta.source === 'steering' is a human interjection (the
44
- // provider layer keeps it as its own user turn for that
45
- // reason) and counts as a human request.
46
- // retry when retryPrompt repeats the trailing unanswered user prompt
47
- // (trailingUnansweredPromptIndex), that stale copy is excluded
48
- // so the prompt is never its own "previous request".
49
- // summary compact summaries are skipped; nothing before them is
50
- // recovered, so history truncated by compaction yields
51
- // 'no-human-request' rather than a guess.
52
- // Limitation: user messages stored without meta (old transcripts) cannot be
53
- // told apart from the human when a runtime wrapper was never recorded; they
54
- // are accepted as human, and a wrapper with stale provenance stays unstripped.
55
- import { stripRuntimeUserContext } from '../agent/orchestrator/session/runtime-user-context.mjs';
56
- import {
57
- isActualUserInstructionMessage,
58
- isInjectedSkillBodyMessage,
59
- isProtectedContextUserMessage,
60
- isSummaryMessage,
61
- } from '../agent/orchestrator/session/compact/messages.mjs';
62
- import { trailingUnansweredPromptIndex } from '../agent/orchestrator/session/manager/failed-turn-rewind.mjs';
63
- import { promptContentText } from '../agent/orchestrator/session/manager/prompt-utils.mjs';
64
-
65
- export const PREVIOUS_HUMAN_POLICY = Object.freeze({
66
- synthetic: 'skip',
67
- steering: 'human',
68
- retry: 'exclude-trailing-unanswered-copy',
69
- summary: 'skip-no-recovery',
70
- });
71
-
72
- const TYPED_NON_HUMAN_SOURCES = new Set(['task-notification', 'skill-context']);
73
-
74
- const textOf = (message) => promptContentText(message?.content).trim();
75
-
76
- /** { human, reason?, message } for a user message; null for other roles. */
77
- function classify(message) {
78
- if (message?.role !== 'user') return null;
79
- const stripped = stripRuntimeUserContext(message);
80
- const meta = stripped.meta && typeof stripped.meta === 'object' ? stripped.meta : {};
81
- const no = (reason) => ({ human: false, reason, message: stripped });
82
- if (isSummaryMessage(stripped)) return no('summary');
83
- if (isProtectedContextUserMessage(stripped)) return no('protected-context');
84
- if (isInjectedSkillBodyMessage(stripped)) return no('skill-body');
85
- if (meta.synthetic === true) return no('synthetic-flag');
86
- if (TYPED_NON_HUMAN_SOURCES.has(String(meta.source || ''))) return no('typed-source');
87
- if (!isActualUserInstructionMessage(stripped)) return no('runtime-owned');
88
- if (!textOf(stripped)) return no('empty');
89
- return { human: true, message: stripped, stripped: stripped !== message };
90
- }
91
-
92
- function latestAssistant(messages, from, to) {
93
- for (let index = to - 1; index >= from; index -= 1) {
94
- if (messages[index]?.role !== 'assistant') continue;
95
- const text = textOf(messages[index]);
96
- if (text) return { text, index };
97
- }
98
- return null;
99
- }
100
-
101
- export function previousHumanContext(messages, { retryPrompt } = {}) {
102
- const list = Array.isArray(messages) ? messages : [];
103
- const retryTrimmedIndex = retryPrompt == null ? -1 : trailingUnansweredPromptIndex(list, retryPrompt);
104
- const boundary = retryTrimmedIndex >= 0 ? retryTrimmedIndex : list.length;
105
- const verdicts = [];
106
- for (let index = 0; index < boundary; index += 1) verdicts.push(classify(list[index]));
107
-
108
- const skipped = [];
109
- let humanIndex = -1;
110
- for (let index = boundary - 1; index >= 0; index -= 1) {
111
- const verdict = verdicts[index];
112
- if (!verdict) continue;
113
- if (verdict.human) {
114
- humanIndex = index;
115
- break;
116
- }
117
- skipped.push({ index, role: 'user', reason: verdict.reason });
118
- // A summary replaces earlier conversational context; do not recover a
119
- // request from an older retained fragment behind that boundary.
120
- if (verdict.reason === 'summary') break;
121
- }
122
-
123
- const canonicalReply = latestAssistant(list, 0, boundary);
124
- let prevHuman = null;
125
- let pairedReply = null;
126
- let pairedMissing = 'no-human-request';
127
- if (humanIndex >= 0) {
128
- const verdict = verdicts[humanIndex];
129
- const source = verdict.message.meta?.source;
130
- prevHuman = {
131
- text: textOf(verdict.message),
132
- index: humanIndex,
133
- source: typeof source === 'string' && source ? source : null,
134
- runtimeContext: verdict.stripped ? 'stripped' : 'none',
135
- };
136
- let end = boundary;
137
- for (let index = humanIndex + 1; index < boundary; index += 1) {
138
- if (verdicts[index]?.human) {
139
- end = index;
140
- break;
141
- }
142
- }
143
- pairedReply = latestAssistant(list, humanIndex + 1, end);
144
- pairedMissing = pairedReply ? null : 'no-assistant-text-in-turn';
145
- }
146
-
147
- let prevHumanMissing = null;
148
- if (!prevHuman) prevHumanMissing = boundary === 0 ? 'empty-history' : 'no-human-request';
149
-
150
- return {
151
- version: 1,
152
- boundary,
153
- retryTrimmedIndex,
154
- prevHuman,
155
- prevHumanMissing,
156
- canonicalReply,
157
- pairedReply,
158
- pairedMissing,
159
- replyIsPaired: Boolean(pairedReply && canonicalReply && pairedReply.index === canonicalReply.index),
160
- skipped,
161
- };
162
- }
@@ -1,10 +0,0 @@
1
- // Experimental input only. An unavailable verified request preserves rp exactly.
2
- // Ownership and pairing are established by the caller, never inferred here.
3
- export function verifiedContextText({ request, prev, prevRequestVerified } = {}) {
4
- const current = String(request || '').slice(0, 1500);
5
- const reply = String(prev || '').slice(-1000);
6
- const prior = Array.from(String(prevRequestVerified || ''))
7
- .slice(0, 300)
8
- .join('');
9
- return `request: ${current}${prior ? `\nprevious request: ${prior}` : ''}\nprevious reply: ${reply}`;
10
- }