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.
- package/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- 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
|
+
}
|