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
@@ -1,6 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { selectedSkillName } from '../../../shared/skill-selection.mjs';
3
+ import { mentionedSkillNames, skillMentionCandidates } from '../../../shared/skill-mentions.mjs';
4
+ import { isAttachmentReference } from '../../../attachments/store.mjs';
5
+ import { collectPromptSkillsCached } from '../context/collect.mjs';
3
6
  import { executeTool } from './loop/tool-exec.mjs';
7
+ import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
4
8
  import { parseNativeToolSearchPayload } from './loop/tool-helpers.mjs';
5
9
  import { normalizeToolEnvelope } from './tool-envelope.mjs';
6
10
  import { throwIfAborted } from '../../../shared/abort-race.mjs';
@@ -20,19 +24,91 @@ function nativeDefinitionsPresent(native, messages) {
20
24
  );
21
25
  }
22
26
 
27
+ // At most this many skills are prepared for one user turn; further selections
28
+ // stay prose.
29
+ const MAX_EXPLICIT_SKILLS = 5;
30
+ const LOADED_SKILL_RE = /^Loaded (?:built-in )?skill: /;
31
+
32
+ // The user's own typed text: plain strings and text parts, never attachment
33
+ // references (file contents the user attached, not words they typed).
34
+ function userPromptText(prompt) {
35
+ if (typeof prompt === 'string') return prompt;
36
+ if (!Array.isArray(prompt)) return '';
37
+ return prompt
38
+ .map((part) => {
39
+ if (typeof part === 'string') return part;
40
+ if (part?.type === 'text' && typeof part.text === 'string' && !isAttachmentReference(part)) return part.text;
41
+ return '';
42
+ })
43
+ .join('\n');
44
+ }
45
+
46
+ /** The skills a prompt explicitly selects: the leading `Skill: "name"` header,
47
+ * then every inline `$name`, each only when it names a skill enabled and in
48
+ * scope for `cwd`, in first-mention order, deduplicated and capped. */
49
+ export function explicitSkillNames(prompt, cwd) {
50
+ const header = selectedSkillName(prompt);
51
+ const text = userPromptText(prompt);
52
+ if (!header && !skillMentionCandidates(text).length) return [];
53
+ const available = new Set(collectPromptSkillsCached(cwd).map((skill) => skill.name));
54
+ const isSkill = (name) => available.has(name);
55
+ const names = header && isSkill(header) ? [header] : [];
56
+ for (const name of mentionedSkillNames(text, isSkill)) {
57
+ if (!names.includes(name)) names.push(name);
58
+ }
59
+ return names.slice(0, MAX_EXPLICIT_SKILLS);
60
+ }
61
+
62
+ /** Stamps the skills a user turn actually loaded on its transcript metadata,
63
+ * where the live row, the persisted transcript and restore all read it.
64
+ * The stamped message REPLACES `message` in `messages` (returned): the
65
+ * session store ships reference-identical messages as an append-only delta,
66
+ * so an in-place edit of a user message an earlier save already sent would
67
+ * never reach disk. */
68
+ export function recordAppliedSkills(messages, message, names) {
69
+ if (!message || !Array.isArray(names) || !names.length) return message;
70
+ const meta = message.meta && typeof message.meta === 'object' ? message.meta : {};
71
+ const transcript = meta.transcript && typeof meta.transcript === 'object' ? meta.transcript : {};
72
+ const stamped = { ...message, meta: { ...meta, transcript: { ...transcript, appliedSkills: [...names] } } };
73
+ const index = Array.isArray(messages) ? messages.indexOf(message) : -1;
74
+ if (index >= 0) messages[index] = stamped;
75
+ return stamped;
76
+ }
77
+
78
+ /** Tells the host which skills a user turn loaded; a host callback failure
79
+ * never fails the turn. */
80
+ export function notifySkillsApplied(onSkillsApplied, applied, message) {
81
+ if (!applied.length) return;
82
+ try {
83
+ onSkillsApplied?.(applied, { messageId: message?.meta?.transcript?.messageId || null });
84
+ } catch {
85
+ /* best-effort host callback */
86
+ }
87
+ }
88
+
23
89
  // Called before the first model request for a real user turn. This performs the
24
90
  // same policy-checked Skill operation, without asking the model to request it.
25
91
  // Native schemas still travel as a properly paired loader execution, never as
26
- // new eager definitions or an orphan tool_search_output.
92
+ // new eager definitions or an orphan tool_search_output. Returns the names of
93
+ // the skills that loaded successfully, in selection order.
94
+ // `selection`, when given (even empty), is the turn's TRUSTED selection input
95
+ // and the only text skills are selected from: a host whose model-visible
96
+ // prompt carries external data (webhook payloads) passes just the
97
+ // operator-authored part, so nothing inside the data can load a skill.
27
98
  export async function prepareExplicitSkills(
28
99
  prompt,
29
100
  messages,
30
101
  session,
31
- { cwd = session?.cwd, signal, execute = executeTool } = {}
102
+ { cwd = session?.cwd, signal, execute = executeTool, selection } = {}
32
103
  ) {
33
104
  throwIfAborted(signal);
34
- const name = selectedSkillName(prompt);
35
- if (name) {
105
+ const applied = [];
106
+ // A session that cannot call Skill (schema allowlist, denied tools, read-only
107
+ // surface — the dispatcher's own gate) has nothing to select: preparing would
108
+ // only push a refused-tool reminder into history on every matching turn.
109
+ if (preDispatchDenyForSession(session, { name: 'Skill' })) return applied;
110
+ const source = selection === undefined || selection === null ? prompt : selection;
111
+ for (const name of explicitSkillNames(source, cwd)) {
36
112
  const id = `skill-selection-${randomUUID()}`;
37
113
  const args = { name };
38
114
  const raw = await execute('Skill', args, cwd, session?.id, session, { signal, toolCallId: id });
@@ -55,6 +131,8 @@ export async function prepareExplicitSkills(
55
131
  });
56
132
  }
57
133
  messages.push(...envelope.newMessages);
134
+ if (LOADED_SKILL_RE.test(String(summary))) applied.push(name);
135
+ throwIfAborted(signal);
58
136
  }
59
- throwIfAborted(signal);
137
+ return applied;
60
138
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Provenance of messages carried over from another app's conversation
3
+ * (conversation-import). Their text is prose someone wrote elsewhere: it may
4
+ * look like a runtime reminder, a task envelope or a compaction summary, but
5
+ * restore and the model wire treat it as plain user/assistant text. Leaf, no IO.
6
+ */
7
+ export const IMPORTED_MESSAGE_SOURCE = 'import';
8
+
9
+ export function isImportedMessage(message) {
10
+ return message?.meta?.source === IMPORTED_MESSAGE_SOURCE;
11
+ }
@@ -2,6 +2,7 @@
2
2
  // (signature + stub) and the edit-progress classification.
3
3
  // Pure string/signature helpers; the loop body only wires state + messages.
4
4
  // No provider/manager coupling.
5
+ import { knownMcpToolIdentity } from '../../../../shared/tool-primitives.mjs';
5
6
 
6
7
  // Deterministic, key-sorted stringify for cross-turn call signatures (recursive,
7
8
  // unlike tool-classify's top-level-only _canonicalArgs).
@@ -43,7 +44,7 @@ const NON_PROGRESS_TOOLS = new Set(['Skill', 'recall', 'agent', 'task', 'cwd', '
43
44
  // not in the meta/non-progress set. apply_patch and shell/bash always count.
44
45
  export function isEditProgressTool(name, isEager) {
45
46
  if (isEager) return false;
46
- const bare = name?.startsWith('mcp__') ? name.split('__').pop() : name;
47
+ const bare = name?.startsWith('mcp__') ? knownMcpToolIdentity(name)?.tool || name.split('__').pop() : name;
47
48
  if (bare === 'apply_patch' || bare === 'shell' || bare === 'bash' || bare === 'bash_session') return true;
48
49
  return !NON_PROGRESS_TOOLS.has(bare);
49
50
  }
@@ -146,7 +146,7 @@ export function createLoopState({ provider, messages, model, tools, cwd, sendOpt
146
146
  messages,
147
147
  opts,
148
148
  sessionId,
149
- onSkillPrompt: (content) => state.pendingSkillPrompts.push(content),
149
+ onSkillPrompt: (content, message) => state.pendingSkillPrompts.push({ content, message }),
150
150
  });
151
151
  // Bounded recovery for provider turns that returned no client tool calls
152
152
  // (max-output ladder, refusal retry, provider continuation, empty-turn
@@ -34,8 +34,9 @@ export function createNoToolTurnResolver({
34
34
  refusalRetryUsed: false,
35
35
  emptyNudgeStreak: 0,
36
36
  // Structured provider continuation signals honored this turn. Diagnostic
37
- // only; the hard iteration cap remains the sole bound on how long a
38
- // provider may keep declaring "not done" inside one user turn.
37
+ // only: the loop has no iteration cap, so this count bounds nothing. A
38
+ // text-only continuation runaway is bounded by continuationsSinceToolBatch
39
+ // below; continuations interleaved with tool batches are not limited.
39
40
  providerContinuationCount: 0,
40
41
  // Continuations since the last executed tool batch — bounds the text-only
41
42
  // continuation runaway (see PROVIDER_CONTINUATION_NO_TOOL_LIMIT).
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { traceCacheBreak } from '../../cache-break-trace.mjs';
7
7
  import { resolveLiveToolCwd } from './tool-exec.mjs';
8
- import { prepareExplicitSkills } from '../explicit-skills.mjs';
8
+ import { notifySkillsApplied, prepareExplicitSkills, recordAppliedSkills } from '../explicit-skills.mjs';
9
9
  import { addUsage, usageDeltaEvent } from './usage.mjs';
10
10
  import { prepareProviderRequest } from './request-boundary.mjs';
11
11
  import { projectProviderRequest } from './request-projection.mjs';
@@ -44,10 +44,13 @@ export async function beginIteration(state) {
44
44
  // policy-checked selection before the next provider snapshot instead of
45
45
  // starting detached work from a drain callback.
46
46
  while (state.pendingSkillPrompts.length) {
47
- await prepareExplicitSkills(state.pendingSkillPrompts.shift(), messages, sessionRef, {
47
+ const { content, message: queued } = state.pendingSkillPrompts.shift();
48
+ const applied = await prepareExplicitSkills(content, messages, sessionRef, {
48
49
  cwd: state.cwd,
49
50
  signal: opts.signal,
50
51
  });
52
+ const message = recordAppliedSkills(messages, queued, applied);
53
+ notifySkillsApplied(opts.onSkillsApplied, applied, message);
51
54
  }
52
55
  const boundary = await prepareProviderRequest({
53
56
  provider,
@@ -263,8 +266,7 @@ function traceActualCacheMiss(state, sent, response, { previousGuardState, previ
263
266
  const ttlMs = MESSAGES_CACHE_TTL_MS[sessionRef.providerCacheOpts?.cacheStrategy?.messages];
264
267
  const idleMs = sent.sendStartedAt - previousSendAt;
265
268
  const requestPrefixChanged =
266
- !!previousGuardState &&
267
- previousGuardState.requestPrefixHash !== sent.prefixGuardCandidate?.requestPrefixHash;
269
+ !!previousGuardState && previousGuardState.requestPrefixHash !== sent.prefixGuardCandidate?.requestPrefixHash;
268
270
  if (
269
271
  baseline > 0 &&
270
272
  ttlMs &&
@@ -156,7 +156,7 @@ function injectSteeringEntry(merged, { messages, opts, stage, onSkillPrompt }) {
156
156
  const notification = merged.mode === 'task-notification';
157
157
  const execution =
158
158
  notification && merged.execution && typeof merged.execution === 'object' ? { ...merged.execution } : null;
159
- messages.push({
159
+ const message = {
160
160
  role: 'user',
161
161
  content: merged.content,
162
162
  meta: {
@@ -165,8 +165,9 @@ function injectSteeringEntry(merged, { messages, opts, stage, onSkillPrompt }) {
165
165
  ...(submissionIds.length ? { submissionIds } : {}),
166
166
  transcript: steeringTranscriptMeta,
167
167
  },
168
- });
169
- if (!notification) onSkillPrompt(merged.content);
168
+ };
169
+ messages.push(message);
170
+ if (!notification) onSkillPrompt(merged.content, message);
170
171
  const text = merged.text || steeringContentText(merged.content);
171
172
  try {
172
173
  opts.onSteerMessage?.(text, {
@@ -12,6 +12,8 @@ import {
12
12
  filterSkillsExcludingDisabled,
13
13
  isSkillDisabled,
14
14
  skillMissingFeature,
15
+ skillOutsideScope,
16
+ skillOutsideScopeError,
15
17
  } from '../../context/collect.mjs';
16
18
 
17
19
  // Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
@@ -96,6 +98,7 @@ export function viewSkill(cwd, name, session = null) {
96
98
  return `Error: skill "${skillName}" needs the ${missingFeature} built-in feature, which is not installed or is switched off in Extensions → Plugin → Built-in`;
97
99
  }
98
100
  if (isSkillDisabled(skillName)) return `Error: skill "${skillName}" is disabled`;
101
+ if (skillOutsideScope(skillName, cwd)) return skillOutsideScopeError(skillName);
99
102
  const res = loadSkillResource(skillName, cwd);
100
103
  if (!res) return `Error: skill "${skillName}" not found`;
101
104
  // Return the general tool envelope: the model-visible tool_result is the
@@ -12,6 +12,17 @@ import {
12
12
 
13
13
  const SYNTHETIC_PROMPT_SOURCES = ['goal-continuation', 'goal-closeout'];
14
14
 
15
+ // A caller-declared prompt source the runtime trusts as provenance of the
16
+ // initial prompt: Goal turns are runtime-synthetic; a task notification (an
17
+ // idle-drained completion, an automation run's agent results) is stored like
18
+ // one the steering/queue drains inject, never as the user speaking — so none
19
+ // of these run explicit skill selection.
20
+ function initialPromptSourceFor(promptSource) {
21
+ if (SYNTHETIC_PROMPT_SOURCES.includes(promptSource)) return { source: promptSource, synthetic: true };
22
+ if (promptSource === PENDING_MODE_TASK_NOTIFICATION) return { source: promptSource };
23
+ return null;
24
+ }
25
+
15
26
  /** Prompt provenance for a drained queue turn: the source plus its execution when one exists. */
16
27
  const promptSourceFields = (source, execution) => {
17
28
  if (!source) return null;
@@ -42,9 +53,7 @@ export function createAskPromptQueue({ sessionId, promptSource }) {
42
53
  // Local FIFO of follow-up prompts drained from the pending-message queue
43
54
  // after each turn — keeps queued `agent type=send` messages in order.
44
55
  const tail = [];
45
- let initialPromptSource = SYNTHETIC_PROMPT_SOURCES.includes(promptSource)
46
- ? { source: promptSource, synthetic: true }
47
- : null;
56
+ let initialPromptSource = initialPromptSourceFor(promptSource);
48
57
 
49
58
  /** The next turn's input — prompt, caller context, consumed queue entries
50
59
  * and provenance — or null when nothing model-visible is left to run. */
@@ -148,7 +148,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
148
148
  // queued send always lands AFTER the in-flight prompt here, ordering is
149
149
  // preserved and the spawn/connecting startup race disappears.
150
150
  for (;;) {
151
- const input = queue.nextTurn({ prompt, context, explicitPrefetch });
151
+ // `skillSelection` (askOpts) is the trusted selection input of the
152
+ // caller's own prompt only; drained follow-up turns never inherit it.
153
+ const input = queue.nextTurn({ prompt, context, explicitPrefetch, skillSelection: askOpts.skillSelection });
152
154
  if (!input) {
153
155
  void takeoverHydration.then((count) => {
154
156
  if (count <= 0) return;
@@ -8,7 +8,12 @@
8
8
  // model declares effort levels (the effort is sent with each request).
9
9
  import { readSection } from '../../../../shared/config.mjs';
10
10
  import { autoEffortModel, turnEffortConfigurationMode } from '../../providers/effort-configuration.mjs';
11
- import { autoEffortBase, normalizeAutoEffortMode, resolveAutoEffort } from '../../providers/auto-effort.mjs';
11
+ import {
12
+ autoEffortApplicable,
13
+ autoEffortBase,
14
+ normalizeAutoEffortMode,
15
+ resolveAutoEffort,
16
+ } from '../../providers/auto-effort.mjs';
12
17
  import { builtinFeatureActive } from '../../runtime-core/builtin-features.mjs';
13
18
  import { effortOptionsFor } from '../../runtime-core/effort.mjs';
14
19
  import { effortJudgeSupportsSteps, judgeTurn, recordEffortDecision } from '../../../../effort-judge/judge-client.mjs';
@@ -78,12 +83,6 @@ export async function resolveTurnAutoEffort({ sessionId, session, provider, inpu
78
83
  if (!turnEffortConfigurationMode(session, provider)) return null;
79
84
  const chosen = String(session.effort || '').toLowerCase();
80
85
  const base = autoEffortBase(chosen);
81
- // The previous user request shows the work a short follow-up continues.
82
- const judged = await judgeTurn({
83
- request,
84
- prev: lastText(session.messages, 'assistant'),
85
- prevRequest: lastText(session.messages, 'user'),
86
- });
87
86
  const record = {
88
87
  at: new Date().toISOString(),
89
88
  sessionId,
@@ -94,15 +93,24 @@ export async function resolveTurnAutoEffort({ sessionId, session, provider, inpu
94
93
  base,
95
94
  requestChars: request.length,
96
95
  };
96
+ const options = effortOptionsFor(session.provider, autoEffortModel(session.provider, provider, session.model));
97
+ // No judged outcome can apply when the base is off the model's ladder, so
98
+ // the judge is not asked (in observe mode too: nothing would be logged).
99
+ if (!autoEffortApplicable({ base, options })) {
100
+ recordEffortDecision({ ...record, skipped: 'default-outside-auto-range' });
101
+ return null;
102
+ }
103
+ // The previous user request shows the work a short follow-up continues.
104
+ const judged = await judgeTurn({
105
+ request,
106
+ prev: lastText(session.messages, 'assistant'),
107
+ prevRequest: lastText(session.messages, 'user'),
108
+ });
97
109
  if (!judged.probs) {
98
110
  recordEffortDecision({ ...record, skipped: judged.skipped });
99
111
  return null;
100
112
  }
101
- const resolved = resolveAutoEffort({
102
- base,
103
- options: effortOptionsFor(session.provider, autoEffortModel(session.provider, provider, session.model)),
104
- probs: judged.probs,
105
- });
113
+ const resolved = resolveAutoEffort({ base, options, probs: judged.probs });
106
114
  recordEffortDecision({
107
115
  ...record,
108
116
  ...(resolved || { skipped: 'default-outside-auto-range' }),
@@ -5,7 +5,7 @@
5
5
  // (runAskAgentLoop). Provider-not-available and trim failures throw here so
6
6
  // the caller marks the session errored instead of leaving stage='connecting'.
7
7
  import { getProvider } from '../../providers/registry.mjs';
8
- import { prepareExplicitSkills } from '../explicit-skills.mjs';
8
+ import { notifySkillsApplied, prepareExplicitSkills, recordAppliedSkills } from '../explicit-skills.mjs';
9
9
  import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
10
10
  import { resolveTurnAutoEffort, turnStepAutoEffort } from './ask-turn-auto-effort.mjs';
11
11
  import { saveSessionAsync } from '../store.mjs';
@@ -151,9 +151,16 @@ export async function runAskAgentLoop({
151
151
  }
152
152
  try {
153
153
  if (!input.promptSource) {
154
- await prepareExplicitSkills(input.prompt, outgoing, session, { cwd: effectiveCwd, signal: turnSignal });
154
+ const applied = await prepareExplicitSkills(input.prompt, outgoing, session, {
155
+ cwd: effectiveCwd,
156
+ signal: turnSignal,
157
+ selection: input.skillSelection,
158
+ });
159
+ // The user turn message sits right after the history it was built on.
160
+ const userMessage = recordAppliedSkills(outgoing, outgoing[prepared.beforeCount - 1], applied);
155
161
  session.messages = filterModelVisibleSessionMessages(outgoing);
156
162
  checkpoint.schedule(true);
163
+ notifySkillsApplied(askOpts?.onSkillsApplied, applied, userMessage);
157
164
  }
158
165
  return await _api_call_with_interrupt(sessionId, (signal) =>
159
166
  agentLoop(
@@ -152,6 +152,10 @@ export function buildAgentLoopOptions({
152
152
  checkpoint.schedule(true);
153
153
  relay(askOpts?.onSteerMessage, text, detail);
154
154
  },
155
+ onSkillsApplied: (names, detail) => {
156
+ checkpoint.schedule(true);
157
+ relay(askOpts?.onSkillsApplied, names, detail);
158
+ },
155
159
  notifyFn: optionalFn(askOpts?.notifyFn),
156
160
  // Same projection compaction uses, so a session presents one identity on
157
161
  // every request it makes.
@@ -158,6 +158,14 @@ export function lateDeliveryText(text, entry, now = Date.now()) {
158
158
  return `[late delivery: queued ${age} ago, before the current session owner started]\n${value}`;
159
159
  }
160
160
 
161
+ // A non-completion entry may be queued explicitly as a task notification (a
162
+ // runtime/channel notice, never the user typing): the mode survives
163
+ // normalization and the durable spool so the drain stores it as one and it
164
+ // never drives explicit skill selection.
165
+ function explicitModeField(entry) {
166
+ return entry?.mode === PENDING_MODE_TASK_NOTIFICATION ? { mode: PENDING_MODE_TASK_NOTIFICATION } : {};
167
+ }
168
+
161
169
  export function normalizePendingMessageEntry(entry) {
162
170
  if (typeof entry === 'string') {
163
171
  const text = entry.trim();
@@ -173,7 +181,10 @@ export function normalizePendingMessageEntry(entry) {
173
181
  id: pendingMessageId(entry),
174
182
  enqueuedAt: Number(entry.enqueuedAt) || Date.now(),
175
183
  };
176
- const entryOptions = entry.options && typeof entry.options === 'object' ? { options: entry.options } : {};
184
+ const entryOptions = {
185
+ ...(entry.options && typeof entry.options === 'object' ? { options: entry.options } : {}),
186
+ ...explicitModeField(entry),
187
+ };
177
188
  const marker =
178
189
  entry.notificationKind === COMPLETION_NOTIFICATION_KIND
179
190
  ? {
@@ -259,26 +270,28 @@ function normalizePersistedEntryBase(entry) {
259
270
  }
260
271
  : null;
261
272
  }
273
+ const mode = explicitModeField(entry);
262
274
  if (typeof entry.message === 'string') {
263
275
  const message = entry.message.trim();
264
- return message ? { id, message, enqueuedAt } : null;
276
+ return message ? { id, message, ...mode, enqueuedAt } : null;
265
277
  }
266
278
  if (Object.hasOwn(entry, 'content')) {
267
279
  const normalized = normalizePendingMessageEntry(entry);
268
280
  if (!normalized) return null;
269
281
  if (typeof normalized.content === 'string' && !normalized.options) {
270
- return { id, message: normalized.text || normalized.content, enqueuedAt };
282
+ return { id, message: normalized.text || normalized.content, ...mode, enqueuedAt };
271
283
  }
272
284
  return {
273
285
  id,
274
286
  content: normalized.content,
275
287
  text: normalized.text,
276
288
  ...(normalized.options ? { options: normalized.options } : {}),
289
+ ...mode,
277
290
  enqueuedAt,
278
291
  };
279
292
  }
280
293
  const text = pendingMessageText(entry);
281
- return text ? { id, message: text, enqueuedAt } : null;
294
+ return text ? { id, message: text, ...mode, enqueuedAt } : null;
282
295
  }
283
296
 
284
297
  export function modelVisiblePendingMessages(messages) {
@@ -72,7 +72,6 @@ export {
72
72
  // attach-on-resume guard) — resolved through the manager facade so the
73
73
  // session-runtime turn API reaches it as mgr.publishSessionPresence.
74
74
  export { publishSessionPresence, deleteSessionPresence } from './store.mjs';
75
- export { isInternalRuntimeNotificationText as _isInternalRuntimeNotificationText } from './manager/prompt-utils.mjs';
76
75
 
77
76
  // ── Usage-metrics surface — re-exported unchanged so loop.mjs / smoke scripts
78
77
  // keep resolving these through the facade. ────────────────────────────────
@@ -408,9 +408,10 @@ function adoptCompactedTranscript(ctx, run) {
408
408
  // providerState so the next send starts a fresh chain.
409
409
  ctx.providerState = undefined;
410
410
  ctx.providerStateCleared = true;
411
- // Compaction shrank the transcript, so prior turns no longer pressure the
412
- // window — reset the iteration counter so a steadily-compacting long task
413
- // isn't killed by the cap, while a non-compacting tight loop still hits it.
411
+ // Compaction starts a new loop epoch: the iteration counter restarts at 0
412
+ // so iteration indices (traces, usage metrics, cross-turn dedup windows)
413
+ // count from the compacted transcript. The counter bounds nothing — the
414
+ // loop has no iteration cap.
414
415
  ctx.iterations = 0;
415
416
  // New loop epoch so persistIterationMetrics idempotency keys do not
416
417
  // collide when iteration indices restart at 1 (incl. iter 1 → iter 1).
@@ -30,6 +30,7 @@ import { _saveAsyncQueued, _saveWorkerPending } from './save-worker.mjs';
30
30
  import { STORED_SESSION_UNREADABLE, _ensureLifecycleFields, _storedSessionFromFile } from './serialize.mjs';
31
31
  import { _savePending } from './pending-saves.mjs';
32
32
  import { isOrdinarySession } from '../store-summary-visibility.mjs';
33
+ import { closeSessionCatalogDb } from '../catalog-db/index.mjs';
33
34
 
34
35
  // Disk mtime of the summary index when the in-memory cache was last refreshed
35
36
  // from it — the cross-process staleness detector.
@@ -145,8 +146,9 @@ function scheduleSessionSummaryIndexRebuild() {
145
146
 
146
147
  /**
147
148
  * Resolves once no summary-index writer of this process is running: the
148
- * background rebuild worker a cold listing (or sweep) spawned has exited and
149
- * every queued index mutation has landed. Both write the index, its lock and
149
+ * background rebuild worker a cold listing (or sweep) spawned has exited,
150
+ * every queued index mutation has landed and the session catalog DB is
151
+ * closed. Both write the index, its lock and
150
152
  * temp files into the data dir after the listing/sweep/save that caused them
151
153
  * has returned, so an owner that removes or switches the data dir awaits this
152
154
  * first.
@@ -159,6 +161,8 @@ export async function settleSessionSummaryIndex() {
159
161
  await new Promise((resolve) => worker.once('exit', resolve));
160
162
  }
161
163
  await settleSummaryIndexWrites();
164
+ // The daemon's session catalog DB holds its file, WAL and lock open.
165
+ await closeSessionCatalogDb();
162
166
  }
163
167
 
164
168
  export function listStoredSessionSummaries(options = {}) {
@@ -11,6 +11,9 @@ import {
11
11
  _removeSessionSummary,
12
12
  _pruneSummaryIndexIds,
13
13
  } from '../store-summary-index.mjs';
14
+ // In the daemon, ids whose file a write just changed are hinted to the
15
+ // session catalog DB (a no-op in any other process).
16
+ import { hintSessionCatalogStorage } from '../catalog-db/index.mjs';
14
17
 
15
18
  // Listing is much hotter than writing, especially while the desktop session
16
19
  // browser is open. Keep the compact sidecar in memory after the first read;
@@ -142,16 +145,19 @@ export function _queueSessionSummaryUpsertRow(row, version = null) {
142
145
  _invalidateSummaryRowsView();
143
146
  }
144
147
  _upsertSessionSummaryRow(durableRow);
148
+ hintSessionCatalogStorage([row.id]);
145
149
  }
146
150
 
147
151
  export function _queueSessionSummaryRemoval(id) {
148
152
  _uncacheSessionSummary(id);
149
153
  _removeSessionSummary(id);
154
+ hintSessionCatalogStorage([id]);
150
155
  }
151
156
 
152
157
  export function _queueSummaryIndexPrune(ids) {
153
158
  for (const id of ids) _uncacheSessionSummary(id);
154
159
  _pruneSummaryIndexIds(ids);
160
+ hintSessionCatalogStorage(ids);
155
161
  }
156
162
 
157
163
  // ── Incremental storage scan ────────────────────────────────────────────────