mixdog 0.9.19 → 0.9.21

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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -34
  3. package/package.json +3 -2
  4. package/scripts/build-tui.mjs +6 -0
  5. package/scripts/hook-bus-test.mjs +8 -0
  6. package/scripts/log-writer-guard-smoke.mjs +131 -0
  7. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  8. package/scripts/recall-bench-cases.json +0 -1
  9. package/scripts/recall-quality-cases.json +1 -2
  10. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  11. package/scripts/tool-smoke.mjs +150 -45
  12. package/src/defaults/skills/setup/SKILL.md +327 -0
  13. package/src/help.mjs +2 -5
  14. package/src/lib/mixdog-debug.cjs +13 -0
  15. package/src/mixdog-session-runtime.mjs +7 -3328
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  17. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
  19. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
  20. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
  23. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  24. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  26. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  27. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  28. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  29. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  30. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  31. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  32. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  34. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  35. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  36. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  37. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  38. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  39. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  40. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  41. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  42. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  44. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  45. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  46. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  47. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  49. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  50. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  51. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  52. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  53. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  55. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  56. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  57. package/src/runtime/channels/index.mjs +6 -2183
  58. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  59. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  60. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  61. package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
  62. package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
  63. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  64. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  65. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  66. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  67. package/src/runtime/channels/lib/worker-main.mjs +777 -0
  68. package/src/runtime/memory/index.mjs +163 -1725
  69. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  70. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  71. package/src/runtime/memory/lib/http-router.mjs +811 -0
  72. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  73. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
  74. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
  75. package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
  76. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  77. package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
  78. package/src/runtime/memory/lib/memory.mjs +39 -0
  79. package/src/runtime/memory/lib/query-handlers.mjs +2 -2
  80. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  81. package/src/runtime/shared/atomic-file.mjs +138 -80
  82. package/src/runtime/shared/child-guardian.mjs +61 -3
  83. package/src/session-runtime/boot-profile.mjs +36 -0
  84. package/src/session-runtime/channel-config-api.mjs +70 -0
  85. package/src/session-runtime/context-status.mjs +181 -0
  86. package/src/session-runtime/env.mjs +17 -0
  87. package/src/session-runtime/lifecycle-api.mjs +242 -0
  88. package/src/session-runtime/model-route-api.mjs +198 -0
  89. package/src/session-runtime/provider-auth-api.mjs +135 -0
  90. package/src/session-runtime/resource-api.mjs +282 -0
  91. package/src/session-runtime/runtime-core.mjs +2104 -0
  92. package/src/session-runtime/session-turn-api.mjs +274 -0
  93. package/src/session-runtime/tool-catalog.mjs +18 -264
  94. package/src/session-runtime/tool-defs.mjs +2 -2
  95. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  96. package/src/standalone/agent-tool.mjs +2 -2
  97. package/src/standalone/channel-worker.mjs +67 -7
  98. package/src/standalone/folder-dialog.mjs +4 -1
  99. package/src/standalone/memory-runtime-proxy.mjs +154 -17
  100. package/src/standalone/seeds.mjs +28 -1
  101. package/src/tui/App.jsx +40 -28
  102. package/src/tui/app/core-memory-picker.mjs +1 -1
  103. package/src/tui/app/doctor.mjs +175 -0
  104. package/src/tui/app/slash-commands.mjs +1 -0
  105. package/src/tui/app/slash-dispatch.mjs +9 -0
  106. package/src/tui/app/use-mouse-input.mjs +6 -0
  107. package/src/tui/app/use-transcript-scroll.mjs +77 -3
  108. package/src/tui/dist/index.mjs +2851 -2162
  109. package/src/tui/engine/context-state.mjs +145 -0
  110. package/src/tui/engine/prompt-history.mjs +27 -0
  111. package/src/tui/engine/session-api-ext.mjs +478 -0
  112. package/src/tui/engine/session-api.mjs +564 -0
  113. package/src/tui/engine/session-flow.mjs +485 -0
  114. package/src/tui/engine/turn.mjs +1078 -0
  115. package/src/tui/engine.mjs +68 -2620
  116. package/src/tui/index.jsx +7 -0
  117. package/vendor/ink/build/ink.js +16 -1
  118. package/vendor/ink/build/output.js +30 -4
  119. package/vendor/ink/build/render.js +5 -0
  120. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,475 @@
1
+ // manager/session-lifecycle.mjs
2
+ // Session build/route/resume lifecycle extracted verbatim from manager.mjs.
3
+ // createSession (spawn), updateSessionRoute (provider/model reroute), and
4
+ // resumeSession (reload tools for a stored session) all share the same tool
5
+ // resolution + context-meta + agent-runtime resolution helpers.
6
+ import { getProvider } from '../../providers/registry.mjs';
7
+ import { normalizeCompactType, DEFAULT_COMPACT_TYPE } from '../compact.mjs';
8
+ import { collectPromptSkillsCached, buildSkillManifest, composeSystemPrompt } from '../../context/collect.mjs';
9
+ import { saveSession, saveSessionAsync, loadSession, setLiveSession } from '../store.mjs';
10
+ import { isAgentOwner } from '../../agent-owner.mjs';
11
+ import { getHiddenAgent } from '../../internal-agents.mjs';
12
+ import { loadConfig } from '../../config.mjs';
13
+ import { buildProviderCacheOpts, cacheCapabilityForProvider } from '../../agent-runtime/cache-strategy.mjs';
14
+ import { normalizeAutoClearConfig, resolveAutoClearIdleMs } from '../../../../../session-runtime/config-helpers.mjs';
15
+ import {
16
+ _buildSharedRules,
17
+ _buildAgentRules,
18
+ _buildLeadRules,
19
+ _buildLeadMetaContext,
20
+ _buildAgentSpecific,
21
+ } from './rules-cache.mjs';
22
+ import {
23
+ applyToolPermissionNarrowing,
24
+ finalizeSessionToolList,
25
+ resolveSessionTools,
26
+ permissionFromToolSpec,
27
+ } from './tool-resolution.mjs';
28
+ import {
29
+ positiveContextWindow,
30
+ preserveBufferConfigFields,
31
+ resolveSessionContextMeta,
32
+ } from './context-meta.mjs';
33
+ import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent-runtime-singleton.mjs';
34
+ import { mintSessionId } from './session-id.mjs';
35
+ import { providerCacheKey } from './provider-cache-key.mjs';
36
+
37
+ // --- agent spawn (createSession) ---
38
+ // opts can pass either a `preset` object (from config.presets) or raw provider/model.
39
+ // Preset shape: { name, provider, model, effort?, fast?, tools? }
40
+ //
41
+ // Agent Runtime integration:
42
+ // opts.taskType / opts.agent / opts.profileId — enables profile-aware routing.
43
+ // Rule-based SmartRouter resolves these synchronously; the resolved
44
+ // profile controls context filtering (skip.skills/memory/etc) and cache
45
+ // strategy. If no rule matches, falls back to classic preset behavior.
46
+ // opts.profile — pre-resolved profile (bypasses router; used by async
47
+ // callers who already ran AgentRuntime.resolve()).
48
+ // opts.providerCacheOpts — pre-resolved cache options merged into ask() sendOpts.
49
+ export function createSession(opts) {
50
+ const presetObj = opts.preset && typeof opts.preset === 'object' ? opts.preset : null;
51
+
52
+ // --- Agent Runtime profile resolution (best-effort, sync) ---
53
+ let profile = opts.profile || null;
54
+ let providerCacheOpts = opts.providerCacheOpts || null;
55
+ if (!profile && (opts.taskType || opts.agent || opts.profileId)) {
56
+ const agentRuntime = getAgentRuntimeSync();
57
+ if (agentRuntime) {
58
+ try {
59
+ const resolved = agentRuntime.resolveSync({
60
+ taskType: opts.taskType,
61
+ agent: opts.agent,
62
+ profileId: opts.profileId,
63
+ preset: presetObj?.name || (typeof opts.preset === 'string' ? opts.preset : null),
64
+ provider: opts.provider || presetObj?.provider,
65
+ });
66
+ if (resolved) {
67
+ profile = resolved.profile;
68
+ providerCacheOpts = resolved.providerCacheOpts;
69
+ }
70
+ } catch (e) {
71
+ // Agent Runtime error — log once, fall back to classic behavior.
72
+ warnAgentRuntimeResolveFailureOnce(e.message);
73
+ }
74
+ }
75
+ }
76
+
77
+ const providerName = opts.provider || presetObj?.provider
78
+ || (profile?.preferredProviders?.[0]);
79
+ const modelName = opts.model || presetObj?.model;
80
+ // opts.tools (caller-supplied) wins over presetObj.tools — caller
81
+ // intent ('tools:readonly' from Pool C, etc.) must override the
82
+ // preset's default 'full'. Previous priority let HAIKU's tools='full'
83
+ // shadow Pool C's explicit readonly request, leaking write tools and
84
+ // bash into a read-only agent.
85
+ const toolPreset = opts.tools || presetObj?.tools || (typeof opts.preset === 'string' ? opts.preset : null) || 'full';
86
+ const effort = Object.prototype.hasOwnProperty.call(opts, 'effort')
87
+ ? (opts.effort || null)
88
+ : (presetObj?.effort || null);
89
+ const fast = presetObj?.fast === true || opts.fast === true;
90
+ if (!providerName)
91
+ throw new Error('createSession: provider is required');
92
+ if (!modelName)
93
+ throw new Error('createSession: model is required');
94
+ const provider = getProvider(providerName);
95
+ if (!provider)
96
+ throw new Error(`Provider "${providerName}" not found or not enabled`);
97
+ const id = mintSessionId();
98
+ // Provider cache strategy — agentRuntime.resolveSync() above is a
99
+ // best-effort injection point (setAgentRuntime() has no live caller
100
+ // today, so that branch never fires); build it directly here so every
101
+ // session still gets a cache strategy. Lead sessions (opts.agent ===
102
+ // 'lead', or no agent at all — raw/CLI callers) get their BP4 message
103
+ // tail TTL linked to the user's autoClear idle-sweep config; hidden and
104
+ // public agents keep the flat 5m default (see cache-strategy.mjs docs).
105
+ // Scoped to explicit-breakpoint (Anthropic-family) providers only — the
106
+ // non-Anthropic branches of buildProviderCacheOpts (e.g. the 'openai'
107
+ // cacheRetention:'24h' shape) were never exercised by createSession
108
+ // before this change, and are left untouched to avoid altering live
109
+ // OpenAI/other-provider request shape as a side effect of this fix.
110
+ if (!providerCacheOpts && cacheCapabilityForProvider(providerName) === 'explicit-breakpoint') {
111
+ try {
112
+ let autoClear = null;
113
+ if (!opts.agent || opts.agent === 'lead') {
114
+ const loadedConfig = loadConfig({ secrets: false });
115
+ const normalizedAutoClear = normalizeAutoClearConfig(loadedConfig?.autoClear);
116
+ autoClear = {
117
+ ...normalizedAutoClear,
118
+ idleMs: resolveAutoClearIdleMs(loadedConfig, providerName),
119
+ };
120
+ }
121
+ providerCacheOpts = buildProviderCacheOpts(providerName, id, opts.agent, { autoClear });
122
+ } catch {
123
+ providerCacheOpts = null;
124
+ }
125
+ }
126
+ const messages = [];
127
+ const ownerIsAgent = isAgentOwner(opts.owner);
128
+ const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
129
+ const hiddenAgent = getHiddenAgent(resolvedAgent);
130
+ const isRetrievalAgent = hiddenAgent?.kind === 'retrieval';
131
+ // Skill schema is fixed for public agent sessions, but hidden retrieval /
132
+ // maintenance roles are deliberately narrowed away from the Skill tool.
133
+ // Do not leak a Skill manifest into those hidden prompts when no Skill()
134
+ // loader is available.
135
+ const skills = (opts.skipSkills || hiddenAgent) ? [] : collectPromptSkillsCached(opts.cwd);
136
+
137
+ // BP1 is shared tool policy (+ compact skill manifest in compose). BP2 is
138
+ // role/system rules. User-defined schedules/webhooks ride as normal user
139
+ // context below so event data does not rewrite BP3 memory/meta.
140
+ const agentRulesAgent = opts.agent || opts.role || profile?.taskType || null;
141
+ const agentRulesProfile = isRetrievalAgent ? 'retrieval' : 'full';
142
+ const skipAgentRules = opts.skipAgentRules === true;
143
+ // BP1 shared tool policy ships to EVERY role (Lead, workers, retrieval,
144
+ // maintenance): its anti-spiral clauses (one anchor is enough, never
145
+ // repeat equivalent patterns/scopes, plausible hit → stop) are exactly
146
+ // what narrow retrieval roles need. Role docs (e.g. 30-explorer.md)
147
+ // override role-inapplicable entries such as the explore routing row.
148
+ const injectedRules = skipAgentRules ? '' : _buildSharedRules();
149
+ const roleRules = skipAgentRules ? '' : (ownerIsAgent ? _buildAgentRules(agentRulesProfile) : _buildLeadRules());
150
+ const metaContext = skipAgentRules ? '' : (ownerIsAgent ? '' : _buildLeadMetaContext());
151
+ const roleSpecific = ownerIsAgent && !skipAgentRules ? _buildAgentSpecific(agentRulesAgent) : '';
152
+ // Prompt permission is metadata for the write bundle, but a read-only role
153
+ // is stamped BEFORE the toolSpec decision so its schema ships the narrowed
154
+ // bundle. Resolve toolPermission (with profile/preset fallbacks) first, and
155
+ // let the stored/logged `permission` reflect that resolved value — not just
156
+ // opts.permission — so diagnostics show the effective read/write class.
157
+ const toolPermission = opts.permission
158
+ || profile?.permission
159
+ || permissionFromToolSpec(toolPreset)
160
+ || null;
161
+ const permission = toolPermission;
162
+
163
+ // Agent sessions do not inherit arbitrary role/profile/preset tool
164
+ // narrowing — that would shatter provider prefix reuse into one shard per
165
+ // role. Instead they collapse onto exactly TWO stable, bit-identical
166
+ // bundles, one cache group each:
167
+ // - read-only roles (reviewer / debugger / hidden retrieval, i.e. any
168
+ // session resolving to permission 'read') -> 'readonly' bundle:
169
+ // read builtins (code_graph/find/glob/list/grep/read) + retrieval
170
+ // (explore/search/web_fetch/Skill), no apply_patch/shell/task, no
171
+ // MCP-write. applyToolPermissionNarrowing('read') below trims the
172
+ // bundle to AGENT_STRING_PERMISSION_READ_ALLOW so the final surface is
173
+ // bit-identical across these roles regardless of MCP registry state.
174
+ // - write roles (worker / heavy-worker / maintainer / …) -> 'full'
175
+ // bundle: the historical full schema.
176
+ // Call-time permission enforcement below is UNCHANGED (defense in depth):
177
+ // applyToolPermissionNarrowing still runs so the bundle choice never
178
+ // widens effective access.
179
+ const isReadOnlyAgentBundle = ownerIsAgent && toolPermission === 'read';
180
+ const toolSpec = ownerIsAgent
181
+ ? (isReadOnlyAgentBundle ? 'readonly' : 'full')
182
+ : (Array.isArray(profile?.tools) ? profile.tools : toolPreset);
183
+ let toolsForRouting = resolveSessionTools(toolSpec, skills, { ownerIsAgentSession: ownerIsAgent });
184
+ // Fail-closed permission intersection: when a session declares an explicit
185
+ // object-form permission, intersect the
186
+ // resolved tool list with the permission's allow/deny lists. If the
187
+ // intersection produces an empty set the permission config is broken —
188
+ // fail closed (zero tools) rather than silently falling back to the full
189
+ // preset, which would grant the role more surface than declared.
190
+ if (ownerIsAgent) {
191
+ toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, toolPermission, opts.agent || null);
192
+ }
193
+
194
+ const { baseRules, stableSystemContext, sessionMarker, volatileTail } = composeSystemPrompt({
195
+ userPrompt: opts.systemPrompt,
196
+ agentRules: injectedRules || undefined,
197
+ roleRules: roleRules || undefined,
198
+ metaContext: metaContext || undefined,
199
+ skipRoleCatalog: !ownerIsAgent,
200
+ profile: profile || undefined,
201
+ agent: resolvedAgent,
202
+ workflowContext: opts.workflowContext || null,
203
+ workspaceContext: opts.workspaceContext || null,
204
+ coreMemoryContext: opts.coreMemoryContext || null,
205
+ skillManifest: buildSkillManifest(skills),
206
+ tools: toolsForRouting,
207
+ bashIsPersistent: ownerIsAgent && toolsForRouting.some(t => t?.name === 'shell'),
208
+ // Effective cwd rides in tier3Reminder so explore-like tools know
209
+ // their search root without needing to shove "Override cwd:" into
210
+ // the user message body (that used to fragment the shard prefix).
211
+ cwd: opts.cwd || null,
212
+ provider: providerName || null,
213
+ });
214
+ // 4-BP layout (see composeSystemPrompt docs):
215
+ // system block #1 = baseRules — BP1 (1h) shared tool policy + skills
216
+ // system block #2 = stableSystemContext — BP2 (1h) role/system rules
217
+ // system block #3 = sessionMarker — BP3 (1h) memory/meta + Profile
218
+ // Preferences (language/name). It rides as a real `system` block so
219
+ // locale/name directives are pinned firmly and do not drift to English
220
+ // after a few turns the way a `user <system-reminder>` reminder did.
221
+ // later normal messages = BP4/tail (task, role data, tool history)
222
+ // Anthropic multi-block system pins each block with cache_control (BP3 is
223
+ // the 3rd system block and carries the tier3 1h marker). OpenAI/xAI get
224
+ // stable provider cache keys/session prefixes. Gemini manages explicit
225
+ // cachedContents inside its provider.
226
+ if (baseRules) {
227
+ messages.push({ role: 'system', content: baseRules });
228
+ }
229
+ if (stableSystemContext) {
230
+ messages.push({ role: 'system', content: stableSystemContext });
231
+ }
232
+ if (sessionMarker) {
233
+ // cacheTier:'tier3' tells the Anthropic providers to pin THIS system
234
+ // block with the tier3 1h cache_control (BP3) — distinct from the
235
+ // BP1/BP2 system TTL. Harmless on non-Anthropic providers (they ignore
236
+ // the field and serialize content as a normal system instruction).
237
+ messages.push({ role: 'system', content: sessionMarker, cacheTier: 'tier3' });
238
+ }
239
+ if (volatileTail) {
240
+ messages.push({ role: 'user', content: `<system-reminder>\n${volatileTail}\n</system-reminder>` });
241
+ messages.push({ role: 'assistant', content: '.' });
242
+ }
243
+ if (roleSpecific) {
244
+ messages.push({ role: 'user', content: `<system-reminder>\n${roleSpecific}\n</system-reminder>` });
245
+ messages.push({ role: 'assistant', content: '.' });
246
+ }
247
+ if (opts.files?.length) {
248
+ const fileContext = opts.files
249
+ .map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
250
+ .join('\n\n');
251
+ messages.push({ role: 'user', content: `Reference files:\n\n${fileContext}` });
252
+ messages.push({ role: 'assistant', content: '.' });
253
+ }
254
+ const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
255
+ const tools = finalizeSessionToolList(toolsForRouting, {
256
+ schemaAllowedTools: hasCallerAllow ? opts.schemaAllowedTools : null,
257
+ disallowedTools: hiddenAgent ? [...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []), 'Skill'] : opts.disallowedTools,
258
+ ownerIsAgent,
259
+ resolvedAgent,
260
+ });
261
+
262
+ // Unified-shard policy — no broad role-specific schema filter. Keep
263
+ // agent schemas shared unless a hidden-role schema profile explicitly
264
+ // passes schemaAllowedTools for a small specialist; broad role
265
+ // whitelists would fragment the cache shard.
266
+ if (resolvedAgent && process.env.MIXDOG_DEBUG_SESSION_LOG) {
267
+ process.stderr.write(`[session] agent=${resolvedAgent} permission=${permission || 'full'} toolPermission=${toolPermission || 'full'} tools=${tools.length}\n`);
268
+ }
269
+ const contextMeta = resolveSessionContextMeta(provider, modelName);
270
+ const workflowMeta = opts.workflow && typeof opts.workflow === 'object' && String(opts.workflow.id || '').trim()
271
+ ? {
272
+ id: String(opts.workflow.id || '').trim(),
273
+ name: String(opts.workflow.name || opts.workflow.id || '').trim(),
274
+ description: String(opts.workflow.description || '').trim(),
275
+ source: String(opts.workflow.source || '').trim(),
276
+ }
277
+ : null;
278
+ const session = {
279
+ id,
280
+ provider: providerName,
281
+ model: modelName,
282
+ messages,
283
+ contextWindow: contextMeta.contextWindow,
284
+ rawContextWindow: contextMeta.rawContextWindow,
285
+ effectiveContextWindowPercent: contextMeta.effectiveContextWindowPercent,
286
+ autoCompactTokenLimit: contextMeta.autoCompactTokenLimit,
287
+ compactBoundaryTokens: contextMeta.compactBoundaryTokens,
288
+ compaction: {
289
+ auto: opts.compaction?.auto !== false,
290
+ prune: opts.compaction?.prune === true,
291
+ semantic: opts.compaction?.semantic ?? 'auto',
292
+ type: normalizeCompactType(opts.compaction?.type ?? opts.compaction?.compactType ?? opts.compaction?.compact_type, DEFAULT_COMPACT_TYPE),
293
+ compactType: normalizeCompactType(opts.compaction?.type ?? opts.compaction?.compactType ?? opts.compaction?.compact_type, DEFAULT_COMPACT_TYPE),
294
+ model: opts.compaction?.model || null,
295
+ timeoutMs: positiveContextWindow(opts.compaction?.timeoutMs),
296
+ tailTurns: positiveContextWindow(opts.compaction?.tailTurns),
297
+ bufferTokens: positiveContextWindow(opts.compaction?.bufferTokens ?? opts.compaction?.buffer),
298
+ // Preserve percent/ratio-named buffer config so the manager/loop/
299
+ // contextStatus parsers (which read bufferPercent/bufferPct/
300
+ // bufferRatio/bufferFraction) can honor it. createSession previously
301
+ // only stored bufferTokens, silently dropping a configured percent.
302
+ ...preserveBufferConfigFields(opts.compaction),
303
+ keepTokens: positiveContextWindow(opts.compaction?.keepTokens ?? opts.compaction?.keep?.tokens),
304
+ preserveRecentTokens: positiveContextWindow(opts.compaction?.preserveRecentTokens),
305
+ reservedTokens: positiveContextWindow(opts.compaction?.reservedTokens),
306
+ recallIngestLimit: positiveContextWindow(opts.compaction?.recallIngestLimit),
307
+ recallChunkLimit: positiveContextWindow(opts.compaction?.recallChunkLimit ?? opts.compaction?.recallLimit),
308
+ recallCycle1BatchSize: positiveContextWindow(opts.compaction?.recallCycle1BatchSize),
309
+ recallRowsPerSession: positiveContextWindow(opts.compaction?.recallRowsPerSession),
310
+ recallWindowSize: positiveContextWindow(opts.compaction?.recallWindowSize),
311
+ recallConcurrency: positiveContextWindow(opts.compaction?.recallConcurrency),
312
+ recallCycle1DeadlineMs: positiveContextWindow(opts.compaction?.recallCycle1DeadlineMs),
313
+ boundaryTokens: contextMeta.compactBoundaryTokens,
314
+ },
315
+ tools,
316
+ preset: toolPreset,
317
+ presetName: presetObj?.name || null,
318
+ effort,
319
+ fast,
320
+ agent: opts.agent,
321
+ owner: opts.owner || 'user',
322
+ mcpPid: process.pid,
323
+ scopeKey: opts.scopeKey || null,
324
+ lane: opts.lane || 'agent',
325
+ cwd: opts.cwd,
326
+ workflow: workflowMeta,
327
+ createdAt: Date.now(),
328
+ updatedAt: Date.now(),
329
+ lastHeartbeatAt: null,
330
+ totalInputTokens: 0,
331
+ totalOutputTokens: 0,
332
+ // Refreshed on each completed ask() — surfaced by agent type=list for
333
+ // debugging + consumed by store.mjs's idle-sweep to reclaim stalled
334
+ // agent sessions past RUNNING_STALL_MS.
335
+ lastUsedAt: Date.now(),
336
+ tokensCumulative: 0,
337
+ taskType: opts.taskType || null,
338
+ maxLoopIterations: Number.isFinite(opts.maxLoopIterations) ? opts.maxLoopIterations : null,
339
+ // Agent tag (auto worker{n} on spawn) persisted so the forked status
340
+ // process (statusline) + aggregator can read it from the session JSON.
341
+ // In-process send/close still resolve via _tagSessionRegistry.
342
+ agentTag: opts.agentTag || null,
343
+ // Prompt permission is separate from runtime toolPermission so preset
344
+ // restrictions do not fragment the agent cache prefix.
345
+ permission: permission || null,
346
+ toolPermission: toolPermission || null,
347
+ schemaAllowedTools: hasCallerAllow ? opts.schemaAllowedTools.map((n) => String(n)) : null,
348
+ // Origin tag written into every agent-trace usage row so analytics
349
+ // can slice by (sourceType, sourceName) — e.g. maintenance/cycle1,
350
+ // scheduler/daily-standup, webhook/github-push, lead/worker.
351
+ sourceType: opts.sourceType || null,
352
+ sourceName: opts.sourceName || null,
353
+ // Provider-scoped unified cache key — one shard per provider,
354
+ // shared across all roles / sources (agent/maintenance/mcp/
355
+ // scheduler/webhook). Role or source-specific context must be
356
+ // injected into the message tail, not the shared prefix.
357
+ promptCacheKey: providerCacheKey(presetObj?.provider || opts.provider, opts.cacheKeyOverride),
358
+ // Agent shell continuity: when an agent session explicitly opts into
359
+ // persistent shell state (`bash` with `persistent:true`, or direct
360
+ // `bash_session`), the minted bash_session id is stored here so later
361
+ // opted-in `bash` calls can reuse the same shell state.
362
+ implicitBashSessionId: null,
363
+ // Tracks every persistent bash session id minted during this
364
+ // orchestrator session so closeSession can kill them all, not just
365
+ // the most recently recorded one.
366
+ allBashSessionIds: [],
367
+ // Agent Runtime metadata — optional. Applied on every ask() to merge
368
+ // profile-driven cache settings into provider sendOpts.
369
+ profileId: profile?.id || null,
370
+ permissionMode: opts.permissionMode ?? null,
371
+ providerCacheOpts: providerCacheOpts || null,
372
+ ownerSessionId: opts.ownerSessionId || null,
373
+ clientHostPid: opts.clientHostPid || null,
374
+ };
375
+ // In-process registry + async debounced save: same-process create → load
376
+ // reads live memory; disk flush is for cross-process / restart durability.
377
+ setLiveSession(session);
378
+ saveSession(session);
379
+ return session;
380
+ }
381
+
382
+ export function updateSessionRoute(id, route = {}) {
383
+ if (!id) return null;
384
+ const session = loadSession(id);
385
+ if (!session || session.closed === true) return null;
386
+ const previousProvider = session.provider || null;
387
+ const previousModel = session.model || null;
388
+ if (route.provider) session.provider = route.provider;
389
+ if (route.model) session.model = route.model;
390
+ if (Object.prototype.hasOwnProperty.call(route, 'fast')) session.fast = route.fast === true;
391
+ if (Object.prototype.hasOwnProperty.call(route, 'effort')) session.effort = route.effort || null;
392
+ const provider = session.provider ? getProvider(session.provider) : null;
393
+ if (provider && session.model) {
394
+ const contextMeta = resolveSessionContextMeta(provider, session.model);
395
+ session.contextWindow = contextMeta.contextWindow;
396
+ session.rawContextWindow = contextMeta.rawContextWindow;
397
+ session.effectiveContextWindowPercent = contextMeta.effectiveContextWindowPercent;
398
+ session.autoCompactTokenLimit = contextMeta.autoCompactTokenLimit;
399
+ session.compactBoundaryTokens = contextMeta.compactBoundaryTokens;
400
+ session.compaction = {
401
+ ...(session.compaction || {}),
402
+ boundaryTokens: contextMeta.compactBoundaryTokens,
403
+ contextWindow: contextMeta.contextWindow,
404
+ rawContextWindow: contextMeta.rawContextWindow,
405
+ effectiveContextWindowPercent: contextMeta.effectiveContextWindowPercent,
406
+ autoCompactTokenLimit: contextMeta.autoCompactTokenLimit,
407
+ };
408
+ } else {
409
+ delete session.contextWindow;
410
+ delete session.rawContextWindow;
411
+ delete session.effectiveContextWindowPercent;
412
+ delete session.autoCompactTokenLimit;
413
+ delete session.compactBoundaryTokens;
414
+ }
415
+ const routeChanged = (route.provider && route.provider !== previousProvider)
416
+ || (route.model && route.model !== previousModel);
417
+ if (routeChanged) {
418
+ const now = Date.now();
419
+ session.lastInputTokens = 0;
420
+ session.lastOutputTokens = 0;
421
+ session.lastCachedReadTokens = 0;
422
+ session.lastCacheWriteTokens = 0;
423
+ session.lastContextTokens = 0;
424
+ session.lastContextTokensUpdatedAt = now;
425
+ session.lastContextTokensStaleAfterCompact = false;
426
+ session.providerState = undefined;
427
+ }
428
+ session.updatedAt = Date.now();
429
+ setLiveSession(session);
430
+ void saveSessionAsync(session, { expectedGeneration: session.generation })
431
+ .catch((err) => {
432
+ try { process.stderr.write(`[session] route update save failed: ${err?.message || err}\n`); } catch {}
433
+ });
434
+ return session;
435
+ }
436
+
437
+ // --- resume (reload tools for a stored session) ---
438
+ export async function resumeSession(sessionId, preset) {
439
+ const session = loadSession(sessionId);
440
+ if (!session)
441
+ return null;
442
+ // Resuming a closed session is a resurrection attempt — refuse. The guarded
443
+ // save below would also block the write, but failing fast here is cleaner
444
+ // than silently dropping the tool-refresh side effects.
445
+ if (session.closed === true) return null;
446
+ if (!session.owner) session.owner = 'user';
447
+ const oldTools = session.tools || [];
448
+ const ownerIsAgent = isAgentOwner(session);
449
+ const skills = ownerIsAgent ? [] : collectPromptSkillsCached(session.cwd);
450
+ let toolSpec = ownerIsAgent ? 'full' : (preset || session.preset || 'full');
451
+ const agentRuntime = getAgentRuntimeSync();
452
+ if (session.profileId && agentRuntime?.getProfile) {
453
+ try {
454
+ const profile = agentRuntime.getProfile(session.profileId);
455
+ if (!ownerIsAgent && Array.isArray(profile?.tools)) toolSpec = profile.tools;
456
+ } catch { /* ignore lookup failures, keep preset fallback */ }
457
+ }
458
+ let toolsForRouting = resolveSessionTools(toolSpec, skills, { ownerIsAgentSession: ownerIsAgent });
459
+ if (ownerIsAgent) {
460
+ toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
461
+ }
462
+ session.tools = finalizeSessionToolList(toolsForRouting, {
463
+ schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
464
+ disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
465
+ ownerIsAgent,
466
+ resolvedAgent: session.agent || null,
467
+ });
468
+ const newTools = session.tools;
469
+ const missing = oldTools.filter(t => !newTools.find(n => n.name === t.name));
470
+ if (missing.length) {
471
+ process.stderr.write(`[session] Warning: ${missing.length} tools no longer available: ${missing.map(t => t.name).join(', ')}\n`);
472
+ }
473
+ await saveSessionAsync(session, { expectedGeneration: session.generation });
474
+ return session;
475
+ }
@@ -0,0 +1,23 @@
1
+ // manager/session-lock.mjs
2
+ // Per-session mutex extracted verbatim from manager.mjs. Queues concurrent
3
+ // askSession calls (and their drained pending-message tail turns) to prevent
4
+ // message loss / interleaving.
5
+ const _sessionLocks = new Map();
6
+ export function acquireSessionLock(sessionId) {
7
+ let entry = _sessionLocks.get(sessionId);
8
+ if (!entry) {
9
+ entry = { promise: Promise.resolve(), count: 0 };
10
+ _sessionLocks.set(sessionId, entry);
11
+ }
12
+ entry.count++;
13
+ const prev = entry.promise;
14
+ let release;
15
+ entry.promise = new Promise(r => { release = r; });
16
+ // Self-heal: if the previous holder rejected, swallow so subsequent
17
+ // queued waiters don't propagate that rejection and brick the lock chain.
18
+ return prev.catch(() => {}).then(() => () => {
19
+ entry.count--;
20
+ if (entry.count === 0) _sessionLocks.delete(sessionId);
21
+ release();
22
+ });
23
+ }