mixdog 1.0.12 → 1.1.0
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/README.md +75 -181
- package/package.json +4 -2
- package/scripts/build-shipped-history.mjs +131 -0
- package/scripts/lib/run-node-tests.mjs +235 -13
- package/scripts/lib/test-failure-records.mjs +5 -0
- package/scripts/lib/test-failure-summary.mjs +9 -3
- package/scripts/lib/test-spawn-ledger.mjs +19 -0
- package/scripts/prune-embedding-runtime.mjs +9 -5
- package/scripts/test-direct.mjs +16 -1
- package/src/agents/advisor/AGENT.md +38 -0
- package/src/agents/advisor/agent.json +6 -0
- package/src/agents/reviewer/AGENT.md +5 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +381 -0
- package/src/defaults/skills/goal-management/SKILL.md +11 -5
- package/src/defaults/skills/setup/SKILL.md +5 -8
- package/src/defaults/skills/setup/references/actions.md +61 -31
- package/src/defaults/skills/setup/references/surfaces.md +12 -10
- package/src/lib/rules-builder.cjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
- package/src/runtime/agent/orchestrator/config.mjs +102 -21
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
- package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
- package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
- package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
- package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
- package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
- package/src/runtime/channels/lib/config.mjs +3 -8
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
- package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
- package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
- package/src/runtime/computer-bridge/actions.mjs +2 -0
- package/src/runtime/effort-judge/judge-client.mjs +12 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
- package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
- package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
- package/src/runtime/memory/lib/memory-embed.mjs +5 -0
- package/src/runtime/shared/agent-route-config.mjs +50 -2
- package/src/runtime/shared/bounded-file-read.mjs +166 -0
- package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
- package/src/runtime/shared/config.mjs +24 -4
- package/src/runtime/shared/defaults-separation.mjs +263 -0
- package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
- package/src/runtime/shared/llm/model-catalog.mjs +23 -0
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
- package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
- package/src/runtime/shared/orchestration.mjs +14 -6
- package/src/runtime/shared/projects.mjs +65 -48
- package/src/runtime/shared/provider-accounts.mjs +47 -6
- package/src/runtime/shared/quota-resume-store.mjs +255 -0
- package/src/runtime/shared/shipped-definitions.mjs +275 -0
- package/src/runtime/shared/tool-card-model.golden.json +3 -3
- package/src/runtime/shared/tool-result-summary.mjs +86 -15
- package/src/runtime/shared/transcript-metadata.mjs +2 -0
- package/src/runtime/shared/turn-review-reply.mjs +2 -0
- package/src/runtime/shared/turn-snapshot.mjs +65 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
- package/src/session-runtime/boot/apis.mjs +19 -0
- package/src/session-runtime/boot/config.mjs +4 -0
- package/src/session-runtime/boot/facade.mjs +4 -1
- package/src/session-runtime/boot/infrastructure.mjs +21 -2
- package/src/session-runtime/boot/session.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
- package/src/session-runtime/config-lifecycle.mjs +31 -6
- package/src/session-runtime/goal-facade-api.mjs +4 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
- package/src/session-runtime/media-api.mjs +32 -22
- package/src/session-runtime/model-recency.mjs +14 -0
- package/src/session-runtime/model-route/route-tuning.mjs +3 -5
- package/src/session-runtime/model-route/set-route.mjs +49 -21
- package/src/session-runtime/model-route-api.mjs +2 -1
- package/src/session-runtime/notification-bus.mjs +31 -6
- package/src/session-runtime/provider-auth/login.mjs +6 -0
- package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
- package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
- package/src/session-runtime/resource-api.mjs +8 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +20 -1
- package/src/session-runtime/runtime-review-api.mjs +12 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +87 -13
- package/src/session-runtime/services/provider-admin.mjs +256 -63
- package/src/session-runtime/services/seeds.mjs +73 -142
- package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
- package/src/session-runtime/services/usage-dashboard.mjs +7 -0
- package/src/session-runtime/session-address.mjs +125 -0
- package/src/session-runtime/session-create.mjs +57 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
- package/src/session-runtime/settings-compaction-api.mjs +13 -0
- package/src/session-runtime/setup-tool/executor.mjs +92 -40
- package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
- package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
- package/src/session-runtime/tool-surface.mjs +16 -9
- package/src/session-runtime/turn/session-ops.mjs +3 -0
- package/src/session-runtime/turn/share-ops.mjs +4 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
- package/src/session-runtime/usage-stats-api.mjs +15 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
- package/src/standalone/daemon-boot-coordinator.mjs +10 -0
- package/src/standalone/daemon-drain-policy.mjs +9 -0
- package/src/standalone/daemon-stored-session-views.mjs +7 -8
- package/src/standalone/daemon.mjs +21 -5
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client.mjs +52 -7
- package/src/standalone/session-protocol.mjs +8 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
- package/src/standalone/session-service/entries/session-loads.mjs +9 -0
- package/src/standalone/session-service/projection/revisions.mjs +28 -4
- package/src/standalone/session-service/projection.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +140 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
- package/src/standalone/session-service/session-calls.mjs +10 -2
- package/src/standalone/session-service/stored-reader.mjs +26 -1
- package/src/standalone/session-service.mjs +21 -0
- package/src/standalone/session-transport/routes.mjs +2 -0
- package/src/tui/app/doctor.mjs +7 -11
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
- package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
- package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
- package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
- package/src/tui/components/ContextPanel.jsx +5 -1
- package/src/tui/components/Spinner.jsx +34 -319
- package/src/tui/components/SpinnerByline.jsx +256 -0
- package/src/tui/components/use-spinner-progress.mjs +113 -0
- package/src/tui/dist/index.mjs +15540 -14452
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
- package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
- package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
- package/src/tui/session/agent-job-feed.mjs +4 -0
- package/src/tui/session/context-state.mjs +13 -1
- package/src/tui/session/initial-state.mjs +6 -0
- package/src/tui/session/quota-resume.mjs +263 -0
- package/src/tui/session/session-api/intake/abort.mjs +39 -35
- package/src/tui/session/session-api/intake/submission.mjs +13 -21
- package/src/tui/session/session-api/intake.mjs +3 -3
- package/src/tui/session/session-api/integrations.mjs +5 -1
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
- package/src/tui/session/session-api/lifecycle.mjs +71 -2
- package/src/tui/session/session-api/media.mjs +2 -2
- package/src/tui/session/session-api/quota-resume.mjs +21 -0
- package/src/tui/session/session-api/routes.mjs +2 -0
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +14 -6
- package/src/tui/session/session-flow/drain.mjs +52 -5
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
- package/src/tui/session/session-flow/queue.mjs +3 -0
- package/src/tui/session/session-flow/steering.mjs +11 -0
- package/src/tui/session/session-flow.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +7 -2
- package/src/tui/session/transcript-intake.mjs +1 -0
- package/src/tui/session/turn.mjs +40 -3
- package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
|
@@ -19,6 +19,22 @@ export function sessionHasRouteHistory(session) {
|
|
|
19
19
|
return hasRouteHistoryMessage(session?.messages) || hasRouteHistoryMessage(session?.liveTurnMessages);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Whether a model change must leave the current conversation on its model:
|
|
23
|
+
// a model reply or a compaction summary ties it to its provider cache and
|
|
24
|
+
// replay state, and a first turn in flight owns its model. Prompts that were
|
|
25
|
+
// all refused before any reply leave nothing to protect.
|
|
26
|
+
export function sessionRouteCommitted(session) {
|
|
27
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
28
|
+
return (
|
|
29
|
+
hasRouteHistoryMessage(session?.liveTurnMessages) ||
|
|
30
|
+
messages.some(
|
|
31
|
+
(message) =>
|
|
32
|
+
message?.role === 'assistant' ||
|
|
33
|
+
(message?.role === 'user' && typeof message.content === 'string' && message.content.startsWith(SUMMARY_PREFIX))
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
export function sessionUsesRoute(session, route) {
|
|
23
39
|
return !!session && session.provider === route?.provider && session.model === route?.model;
|
|
24
40
|
}
|
|
@@ -305,6 +305,8 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
|
|
|
305
305
|
}
|
|
306
306
|
const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
|
|
307
307
|
const defaultNames = defaultDeferredToolNames(catalog, mode);
|
|
308
|
+
// Solo keeps `agent` in the deferred pool: loadable on request, never eager.
|
|
309
|
+
if (session.orchestrationMode === 'none') defaultNames.delete('agent');
|
|
308
310
|
const storedNames = providerMode === 'native' ? [] : storedDeferredToolNames(session);
|
|
309
311
|
const fixedSurface = FIXED_SURFACE_MODES.has(providerMode);
|
|
310
312
|
let selectedNames = fixedSurface
|
|
@@ -144,8 +144,8 @@ export function collectStandaloneToolDefs({
|
|
|
144
144
|
return {
|
|
145
145
|
standaloneTools,
|
|
146
146
|
internalToolDefs: [...standaloneTools, ...webSearchRuntimeTools.filter((tool) => tool?.public === false)],
|
|
147
|
-
// Workflow-aware model surface: a
|
|
148
|
-
//
|
|
147
|
+
// Workflow-aware model surface: a session with no delegatable agents
|
|
148
|
+
// must not advertise the agent tool at all — the model calling a
|
|
149
149
|
// schema-visible tool that policy always rejects is a guaranteed error turn
|
|
150
150
|
// (user-reported in Solo). Names derive from the live agent tool defs.
|
|
151
151
|
agentToolNames: new Set(agentTools.map((tool) => String(tool?.name || '')).filter(Boolean)),
|
|
@@ -6,7 +6,7 @@ import { orchestrationInstructions } from '../orchestration.mjs';
|
|
|
6
6
|
import { DEFAULT_WORKFLOW_ID, normalizeWorkflowId } from '../workflow-ids.mjs';
|
|
7
7
|
|
|
8
8
|
export function createWorkflowContext({ packs, agents }) {
|
|
9
|
-
function workflowSummary(pack, { hasAgents = true
|
|
9
|
+
function workflowSummary(pack, { hasAgents = true } = {}) {
|
|
10
10
|
const id = normalizeWorkflowId(pack?.id, DEFAULT_WORKFLOW_ID);
|
|
11
11
|
return {
|
|
12
12
|
id,
|
|
@@ -14,7 +14,8 @@ export function createWorkflowContext({ packs, agents }) {
|
|
|
14
14
|
description: clean(pack?.description),
|
|
15
15
|
source: clean(pack?.source),
|
|
16
16
|
// Effective session capability, not an editable workflow property.
|
|
17
|
-
|
|
17
|
+
// Solo keeps the agent tool deferred for explicit user requests.
|
|
18
|
+
delegatesAgents: hasAgents !== false,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -23,7 +24,6 @@ export function createWorkflowContext({ packs, agents }) {
|
|
|
23
24
|
function activeWorkflowSummary(config, dir) {
|
|
24
25
|
return workflowSummary(packs.sharedWorkflowPack(dir, packs.activeWorkflowId(config)), {
|
|
25
26
|
hasAgents: agents.delegatableAgentIds(config, dir).length > 0,
|
|
26
|
-
orchestrationMode: configuredOrchestrationMode(config),
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -44,10 +44,11 @@ export function createWorkflowContext({ packs, agents }) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function orchestrationContextBlock(config, dir) {
|
|
47
|
-
const
|
|
48
|
-
if (!instructions) return '';
|
|
49
|
-
const lines = [instructions];
|
|
47
|
+
const mode = configuredOrchestrationMode(config);
|
|
50
48
|
const agentIds = agents.delegatableAgentIds(config, dir);
|
|
49
|
+
// Solo without agents has nothing to delegate to on request either.
|
|
50
|
+
if (mode === 'none' && !agentIds.length) return '';
|
|
51
|
+
const lines = [orchestrationInstructions(mode)];
|
|
51
52
|
const agentBlocks = agentIds.map((id) => agents.loadAgentDefinition(dir, id)).filter(Boolean);
|
|
52
53
|
if (agentBlocks.length) {
|
|
53
54
|
lines.push('# Available Agents');
|
|
@@ -74,7 +75,6 @@ export function createWorkflowContext({ packs, agents }) {
|
|
|
74
75
|
return {
|
|
75
76
|
summary: workflowSummary(pack, {
|
|
76
77
|
hasAgents: agents.delegatableAgentIds(config, dir).length > 0,
|
|
77
|
-
orchestrationMode,
|
|
78
78
|
}),
|
|
79
79
|
orchestrationMode,
|
|
80
80
|
// Pack block alone: agent-owned sessions never get the Lead-only
|
|
@@ -187,12 +187,18 @@ export function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
|
187
187
|
const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
188
188
|
const fast = routeLike?.fast ?? fallback.fast;
|
|
189
189
|
const modelParameters = routeLike?.modelParameters ?? fallback.modelParameters;
|
|
190
|
+
const requestedPercent = Number(routeLike?.contextPercent ?? fallback.contextPercent);
|
|
191
|
+
const contextPercent =
|
|
192
|
+
Number.isFinite(requestedPercent) && requestedPercent >= 10 && requestedPercent <= 100
|
|
193
|
+
? Math.round(requestedPercent / 10) * 10
|
|
194
|
+
: null;
|
|
190
195
|
return {
|
|
191
196
|
provider,
|
|
192
197
|
model,
|
|
193
198
|
...(effort ? { effort } : {}),
|
|
194
199
|
...(typeof fast === 'boolean' ? { fast } : {}),
|
|
195
200
|
...(modelParameters && typeof modelParameters === 'object' ? { modelParameters: { ...modelParameters } } : {}),
|
|
201
|
+
...(contextPercent ? { contextPercent } : {}),
|
|
196
202
|
};
|
|
197
203
|
}
|
|
198
204
|
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* later request rebuilds the same history and the prompt cache keeps hitting.
|
|
9
9
|
* A judge that is missing, slow or failing leaves the step at the effort in
|
|
10
10
|
* force (the turn's or the previous step's).
|
|
11
|
+
* A custom route (snapshot mode 'custom') has no mid-conversation update: the
|
|
12
|
+
* judged effort, snapped onto the model's levels, becomes the effort of the
|
|
13
|
+
* turn's following requests.
|
|
11
14
|
*/
|
|
12
15
|
import { resolveAutoEffort } from '../../providers/auto-effort.mjs';
|
|
13
|
-
import {
|
|
16
|
+
import { resolveCustomReasoning } from '../../providers/custom-reasoning.mjs';
|
|
17
|
+
import { autoEffortModel, stepEffortConfiguration } from '../../providers/effort-configuration.mjs';
|
|
14
18
|
import { effortOptionsFor } from '../../runtime-core/effort.mjs';
|
|
15
19
|
import { judgeTurn, recordEffortDecision } from '../../../../effort-judge/judge-client.mjs';
|
|
16
20
|
import { stepResultWindow } from '../../../../effort-judge/judge-input.mjs';
|
|
@@ -68,14 +72,22 @@ export async function markStepEffort(state, assistantTurnMsg) {
|
|
|
68
72
|
},
|
|
69
73
|
});
|
|
70
74
|
// `atStep` numbers the tool-result step; `step` in a resolved decision is the ladder move.
|
|
71
|
-
const record = {
|
|
75
|
+
const record = {
|
|
76
|
+
at: new Date().toISOString(),
|
|
77
|
+
sessionId: state.sessionId,
|
|
78
|
+
provider,
|
|
79
|
+
model,
|
|
80
|
+
mode: 'on',
|
|
81
|
+
atStep: step,
|
|
82
|
+
};
|
|
72
83
|
if (!judged.probs) {
|
|
73
84
|
recordEffortDecision({ ...record, skipped: judged.skipped });
|
|
74
85
|
return;
|
|
75
86
|
}
|
|
87
|
+
const modelMeta = autoEffortModel(provider, state.provider, model);
|
|
76
88
|
const resolved = resolveAutoEffort({
|
|
77
89
|
base: auto.base,
|
|
78
|
-
options: effortOptionsFor(provider,
|
|
90
|
+
options: effortOptionsFor(provider, modelMeta),
|
|
79
91
|
probs: judged.probs,
|
|
80
92
|
});
|
|
81
93
|
recordEffortDecision({
|
|
@@ -84,6 +96,19 @@ export async function markStepEffort(state, assistantTurnMsg) {
|
|
|
84
96
|
ms: judged.ms,
|
|
85
97
|
probs: judged.probs.map((value) => Number(value.toFixed(3))),
|
|
86
98
|
});
|
|
99
|
+
if (snapshot.mode === 'custom') {
|
|
100
|
+
const level =
|
|
101
|
+
resolved &&
|
|
102
|
+
resolveCustomReasoning({
|
|
103
|
+
protocol: state.provider?.customConfig?.protocol,
|
|
104
|
+
model: modelMeta,
|
|
105
|
+
effort: resolved.effort,
|
|
106
|
+
}).level;
|
|
107
|
+
if (!level) return;
|
|
108
|
+
state.opts.effort = level;
|
|
109
|
+
last.meta = { ...last.meta, effortConfiguration: { ...snapshot, effort: level } };
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
87
112
|
const marked = resolved && stepEffortConfiguration(snapshot, provider, model, resolved.effort);
|
|
88
113
|
if (marked) last.meta = { ...last.meta, effortConfiguration: marked };
|
|
89
114
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Auto effort for one user turn. The Auto effort built-in (Extensions →
|
|
2
2
|
// Built-in) turns it `on`: the judged effort applies to this turn. The
|
|
3
3
|
// MIXDOG_AUTO_EFFORT env value (off/observe/on) overrides it for diagnostics;
|
|
4
|
-
// `observe` judges and logs without applying. Applies
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// `observe` judges and logs without applying. Applies to user-written prompts
|
|
5
|
+
// and to agent sessions' briefs and Lead follow-ups (not runtime-injected
|
|
6
|
+
// turns) on models whose effort can change mid-conversation without
|
|
7
|
+
// invalidating the prompt cache, and on custom Chat/Responses routes whose
|
|
8
|
+
// model declares effort levels (the effort is sent with each request).
|
|
7
9
|
import { readSection } from '../../../../shared/config.mjs';
|
|
8
|
-
import {
|
|
10
|
+
import { autoEffortModel, turnEffortConfigurationMode } from '../../providers/effort-configuration.mjs';
|
|
9
11
|
import { autoEffortBase, normalizeAutoEffortMode, resolveAutoEffort } from '../../providers/auto-effort.mjs';
|
|
10
12
|
import { builtinFeatureActive } from '../../runtime-core/builtin-features.mjs';
|
|
11
13
|
import { effortOptionsFor } from '../../runtime-core/effort.mjs';
|
|
12
|
-
import { judgeTurn, recordEffortDecision } from '../../../../effort-judge/judge-client.mjs';
|
|
14
|
+
import { effortJudgeSupportsSteps, judgeTurn, recordEffortDecision } from '../../../../effort-judge/judge-client.mjs';
|
|
13
15
|
import { promptContentText } from './prompt-utils.mjs';
|
|
14
16
|
|
|
15
17
|
// Every cache-safe route judges its tool-result steps the same way. Measured
|
|
@@ -26,6 +28,17 @@ export function autoEffortStepsEnabled() {
|
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/** `{ request, base }` when each tool-result step of this turn is judged
|
|
32
|
+
* (session/loop/step-auto-effort.mjs): auto effort is on for the turn, the
|
|
33
|
+
* route has an effort configuration (a mid-conversation update route, or a
|
|
34
|
+
* custom Chat/Responses route whose model declares levels), and the installed
|
|
35
|
+
* judge was trained on steps. Otherwise null. */
|
|
36
|
+
export function turnStepAutoEffort(autoEffort, effortConfiguration) {
|
|
37
|
+
return autoEffort?.mode === 'on' && effortConfiguration && autoEffortStepsEnabled() && effortJudgeSupportsSteps()
|
|
38
|
+
? { request: autoEffort.request, base: autoEffort.base }
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
export function autoEffortMode() {
|
|
30
43
|
if (process.env.MIXDOG_AUTO_EFFORT) return normalizeAutoEffortMode(process.env.MIXDOG_AUTO_EFFORT);
|
|
31
44
|
try {
|
|
@@ -53,14 +66,14 @@ function lastText(messages, role) {
|
|
|
53
66
|
*/
|
|
54
67
|
export async function resolveTurnAutoEffort({ sessionId, session, provider, input }) {
|
|
55
68
|
const mode = autoEffortMode();
|
|
56
|
-
// promptSource marks runtime-injected turns (completion notices, queued work)
|
|
57
|
-
//
|
|
58
|
-
|
|
69
|
+
// promptSource marks runtime-injected turns (completion notices, queued work).
|
|
70
|
+
// Agent sessions are judged like any other: their spawn brief and later Lead
|
|
71
|
+
// `send` messages arrive as plain prompts.
|
|
72
|
+
if (mode === 'off' || input?.promptSource) return null;
|
|
59
73
|
const request = promptContentText(input?.prompt).trim();
|
|
60
74
|
// Skill bodies and runtime blocks arrive as tagged text, not as a request.
|
|
61
75
|
if (!request || request.startsWith('<')) return null;
|
|
62
|
-
|
|
63
|
-
if (!effortConfigurationMode(session.provider, session.model, opts)) return null;
|
|
76
|
+
if (!turnEffortConfigurationMode(session, provider)) return null;
|
|
64
77
|
const chosen = String(session.effort || '').toLowerCase();
|
|
65
78
|
const base = autoEffortBase(chosen);
|
|
66
79
|
// The previous user request shows the work a short follow-up continues.
|
|
@@ -85,7 +98,7 @@ export async function resolveTurnAutoEffort({ sessionId, session, provider, inpu
|
|
|
85
98
|
}
|
|
86
99
|
const resolved = resolveAutoEffort({
|
|
87
100
|
base,
|
|
88
|
-
options: effortOptionsFor(session.provider,
|
|
101
|
+
options: effortOptionsFor(session.provider, autoEffortModel(session.provider, provider, session.model)),
|
|
89
102
|
probs: judged.probs,
|
|
90
103
|
});
|
|
91
104
|
recordEffortDecision({
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
import { getProvider } from '../../providers/registry.mjs';
|
|
8
8
|
import { prepareExplicitSkills } from '../explicit-skills.mjs';
|
|
9
9
|
import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
|
|
10
|
-
import {
|
|
11
|
-
import { effortJudgeSupportsSteps } from '../../../../effort-judge/judge-client.mjs';
|
|
10
|
+
import { resolveTurnAutoEffort, turnStepAutoEffort } from './ask-turn-auto-effort.mjs';
|
|
12
11
|
import { saveSessionAsync } from '../store.mjs';
|
|
13
12
|
import { hasUserConversationMessage, refreshSessionBp3Environment } from './prompt-utils.mjs';
|
|
14
13
|
import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
|
|
@@ -37,10 +36,7 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
|
|
|
37
36
|
);
|
|
38
37
|
// Auto effort also judges every tool-result step of the turn when the
|
|
39
38
|
// installed judge was trained on steps (session/loop/step-auto-effort.mjs).
|
|
40
|
-
const stepAutoEffort =
|
|
41
|
-
autoEffort?.mode === 'on' && effortConfiguration && autoEffortStepsEnabled() && effortJudgeSupportsSteps()
|
|
42
|
-
? { request: autoEffort.request, base: autoEffort.base }
|
|
43
|
-
: null;
|
|
39
|
+
const stepAutoEffort = turnStepAutoEffort(autoEffort, effortConfiguration);
|
|
44
40
|
applyTurnContextMeta(session, provider);
|
|
45
41
|
const effectiveCwd = cwdOverride || session.cwd;
|
|
46
42
|
// A failed-turn retry resubmits the prompt the failure left unanswered;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// resumeSession (reload tools for a stored session) all share the same tool
|
|
5
5
|
// resolution + context-meta + agent-runtime resolution helpers.
|
|
6
6
|
import { getProvider } from '../../providers/registry.mjs';
|
|
7
|
+
import { invalidateContextUsageSnapshot, invalidateProviderContextBaseline } from '../loop/compact-policy.mjs';
|
|
7
8
|
import { saveSession, saveSessionAsync, saveSessionAsyncDeferred, loadSession, setLiveSession } from '../store.mjs';
|
|
9
|
+
import { _liveSessions } from '../store/live-state.mjs';
|
|
8
10
|
import { _isActivelyOwnedElsewhere, _recoverTurnCheckpointDurably } from './session-owner-liveness.mjs';
|
|
9
11
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
10
12
|
import { getHiddenAgent } from '../../internal-agents.mjs';
|
|
@@ -44,7 +46,7 @@ export { isSessionOwnerGone, recoverSessionAfterProcessRestart } from './session
|
|
|
44
46
|
// re-exported so prior importers of this module stay unchanged.
|
|
45
47
|
export { prefetchSession, prepareSessionProjection } from './prepared-resume-cache.mjs';
|
|
46
48
|
|
|
47
|
-
function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
|
|
49
|
+
function buildSessionProviderCacheOpts(providerName, sessionId, agent = null, config = null) {
|
|
48
50
|
// Keep this in sync with createSession's provider-cache policy: only
|
|
49
51
|
// explicit-breakpoint providers get BP cache opts here; OpenAI/key-prefix
|
|
50
52
|
// providers use promptCacheKey and request-time strategy instead, plus the
|
|
@@ -55,7 +57,7 @@ function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
|
|
|
55
57
|
try {
|
|
56
58
|
let autoClear = null;
|
|
57
59
|
if (!agent || agent === 'lead') {
|
|
58
|
-
const loadedConfig = loadConfig({ secrets: false });
|
|
60
|
+
const loadedConfig = config || loadConfig({ secrets: false });
|
|
59
61
|
const normalizedAutoClear = normalizeAutoClearConfig(loadedConfig?.autoClear);
|
|
60
62
|
autoClear = {
|
|
61
63
|
...normalizedAutoClear,
|
|
@@ -68,6 +70,29 @@ function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// Re-derive a Lead session's builder-made cache policy (messages TTL follows
|
|
74
|
+
// the auto-clear idle window). Explicit caller/profile policies, non-Lead
|
|
75
|
+
// sessions and non-explicit-breakpoint providers are left untouched.
|
|
76
|
+
export function refreshSessionProviderCacheOpts(session, config = null) {
|
|
77
|
+
if (!session || session.providerCacheOptsOverride === true) return false;
|
|
78
|
+
if (session.agent && session.agent !== 'lead') return false;
|
|
79
|
+
if (cacheCapabilityForProvider(session.provider) !== 'explicit-breakpoint') return false;
|
|
80
|
+
const next = buildSessionProviderCacheOpts(session.provider, session.id, session.agent, config);
|
|
81
|
+
if (!next) return false;
|
|
82
|
+
session.providerCacheOpts = next;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// An auto-clear settings change reaches every Lead session held in this
|
|
87
|
+
// process, not only the one on screen; persisted ones refresh on resume.
|
|
88
|
+
export function refreshLiveSessionsProviderCacheOpts(config = null) {
|
|
89
|
+
let refreshed = 0;
|
|
90
|
+
for (const session of _liveSessions.values()) {
|
|
91
|
+
if (refreshSessionProviderCacheOpts(session, config)) refreshed += 1;
|
|
92
|
+
}
|
|
93
|
+
return refreshed;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
// --- agent spawn (createSession) ---
|
|
72
97
|
// opts can pass either a `preset` object (from config.presets) or raw provider/model.
|
|
73
98
|
// Preset shape: { name, provider, model, effort?, fast?, tools? }
|
|
@@ -199,6 +224,7 @@ export function createSession(opts) {
|
|
|
199
224
|
contextMeta,
|
|
200
225
|
origin: sessionOriginFields(opts, { profile, presetObj, providerCacheOpts, surface }),
|
|
201
226
|
});
|
|
227
|
+
session.providerCacheOptsOverride = Boolean(resolved.providerCacheOpts);
|
|
202
228
|
refreshSessionBp3Environment(session, opts.cwd);
|
|
203
229
|
// In-process registry + async debounced save: same-process create → load
|
|
204
230
|
// reads live memory; disk flush is for cross-process / restart durability.
|
|
@@ -308,6 +334,7 @@ function applyRouteContextWindow(session, routeChanged, selectedContextWindowPro
|
|
|
308
334
|
function resetSessionForRouteChange(id, session, previousModel, previousProvider) {
|
|
309
335
|
session.promptCacheKey = providerCacheKey(session.provider);
|
|
310
336
|
session.providerCacheOpts = buildSessionProviderCacheOpts(session.provider, session.id, session.agent) || null;
|
|
337
|
+
session.providerCacheOptsOverride = false;
|
|
311
338
|
session.lastInputTokens = 0;
|
|
312
339
|
session.lastOutputTokens = 0;
|
|
313
340
|
session.lastCachedReadTokens = 0;
|
|
@@ -337,7 +364,34 @@ function resetSessionForRouteChange(id, session, previousModel, previousProvider
|
|
|
337
364
|
_forgetPreparedResume(id);
|
|
338
365
|
}
|
|
339
366
|
|
|
340
|
-
|
|
367
|
+
// Every message field a provider adapter replays or re-reads as native state.
|
|
368
|
+
const PROVIDER_REPLAY_FIELDS = [
|
|
369
|
+
'providerReplay',
|
|
370
|
+
'reasoning',
|
|
371
|
+
'thinkingBlocks',
|
|
372
|
+
'assistantBlocks',
|
|
373
|
+
'reasoningItems',
|
|
374
|
+
'reasoningContent',
|
|
375
|
+
'providerMetadata',
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
// Provider-native replay (reasoning items, thinking blocks) belongs to the
|
|
379
|
+
// provider that produced it. Replace, never mutate, so delta saves rewrite.
|
|
380
|
+
function stripProviderReplay(session) {
|
|
381
|
+
if (!Array.isArray(session.messages)) return;
|
|
382
|
+
session.messages = session.messages.map((message) => {
|
|
383
|
+
if (!message || !PROVIDER_REPLAY_FIELDS.some((field) => field in message)) return message;
|
|
384
|
+
const plain = { ...message };
|
|
385
|
+
for (const field of PROVIDER_REPLAY_FIELDS) delete plain[field];
|
|
386
|
+
return plain;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// `options.crossProvider` is the explicit in-place continuation of an
|
|
391
|
+
// established session on another model: the previous provider's replay state
|
|
392
|
+
// and context readings are dropped so the next turn re-measures (and
|
|
393
|
+
// auto-compacts) against the new model's window.
|
|
394
|
+
export function updateSessionRoute(id, route = {}, options = {}) {
|
|
341
395
|
if (!id) return null;
|
|
342
396
|
const session = loadSession(id);
|
|
343
397
|
if (!session || session.closed === true) return null;
|
|
@@ -348,6 +402,11 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
348
402
|
(route.provider && route.provider !== previousProvider) || (route.model && route.model !== previousModel);
|
|
349
403
|
applyRouteContextWindow(session, routeChanged, Object.hasOwn(route, 'selectedContextWindow'));
|
|
350
404
|
if (routeChanged) resetSessionForRouteChange(id, session, previousModel, previousProvider);
|
|
405
|
+
if (routeChanged && options.crossProvider === true) {
|
|
406
|
+
stripProviderReplay(session);
|
|
407
|
+
invalidateContextUsageSnapshot(session);
|
|
408
|
+
invalidateProviderContextBaseline(session);
|
|
409
|
+
}
|
|
351
410
|
// The model feeds the `# Session` prompt block (`Model: …`).
|
|
352
411
|
// Rebuild it here: createSession stamped the block with the creation-time
|
|
353
412
|
// route and set sessionStartMetaInjected, so the ask-time refresh guard
|
|
@@ -449,6 +508,7 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
449
508
|
if (_isActivelyOwnedElsewhere(session, sessionId)) return _attachViewerSession(session, sessionId);
|
|
450
509
|
_recoverTurnCheckpointDurably(session, sessionId);
|
|
451
510
|
_refreshResumedTools(session, sessionId, preset);
|
|
511
|
+
refreshSessionProviderCacheOpts(session);
|
|
452
512
|
// The live session already owns the refreshed tools and desktop scope.
|
|
453
513
|
// Defer the structured clone + worker round-trip so opening a conversation
|
|
454
514
|
// is not blocked on persisting the same in-memory state back to disk.
|
|
@@ -9,15 +9,15 @@ import { collectPromptSkillsCached } from '../../context/collect.mjs';
|
|
|
9
9
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
10
10
|
import { getHiddenAgent } from '../../internal-agents.mjs';
|
|
11
11
|
import { workflowDisallowsAgentTool } from '../../runtime-core/workflow.mjs';
|
|
12
|
-
import { sessionOrchestrationMode } from '../../../../shared/orchestration.mjs';
|
|
13
12
|
import { finalizeSessionToolList, resolveSessionTools, permissionFromToolSpec } from './tool-resolution.mjs';
|
|
14
13
|
import { unusedModelEditToolName } from '../../../../shared/edit-tool-dialect.mjs';
|
|
15
14
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
16
15
|
|
|
17
|
-
// A lead session may not delegate when
|
|
18
|
-
//
|
|
16
|
+
// A lead session may not delegate when its workflow forbids the agent tool
|
|
17
|
+
// (no delegatable agents); Solo keeps it deferred for explicit user requests.
|
|
18
|
+
// An agent-owned session never gets the tool anyway.
|
|
19
19
|
export function delegationDisabled(source, ownerIsAgent) {
|
|
20
|
-
return !ownerIsAgent &&
|
|
20
|
+
return !ownerIsAgent && workflowDisallowsAgentTool(source?.workflow);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Exactly two schema surfaces exist: Lead and Agent. Every Agent role gets
|
|
@@ -52,6 +52,7 @@ function restoredTranscriptMetadata(message) {
|
|
|
52
52
|
...(Number.isFinite(Number(value.at)) ? { at: Number(value.at) } : {}),
|
|
53
53
|
...transcriptRouteMetadataFields(value),
|
|
54
54
|
...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
|
|
55
|
+
...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
|
|
55
56
|
...(completion ? { completion } : {}),
|
|
56
57
|
};
|
|
57
58
|
}
|
|
@@ -6,6 +6,7 @@ import { Readable } from 'node:stream';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
|
|
8
8
|
import { hashText } from './hash-utils.mjs';
|
|
9
|
+
import { fileVersion, sameFileVersion } from './file-version.mjs';
|
|
9
10
|
import { sleep } from '../../../../shared/sleep.mjs';
|
|
10
11
|
import { ioTraceDone, ioTraceStart } from '../io-trace.mjs';
|
|
11
12
|
|
|
@@ -20,13 +21,7 @@ function expectedTargetSnapshotChanged(currentStat, expected) {
|
|
|
20
21
|
const currentExists = !!currentStat;
|
|
21
22
|
if (snapshotExists !== currentExists) return true;
|
|
22
23
|
if (!snapshotExists || !currentExists) return false;
|
|
23
|
-
|
|
24
|
-
if (Math.abs(Number(currentStat.mtimeMs) - Number(expected.mtimeMs)) > 1) return true;
|
|
25
|
-
if (Number.isFinite(expected.ctimeMs)) {
|
|
26
|
-
if (Math.abs(Number(currentStat.ctimeMs) - Number(expected.ctimeMs)) > 1) return true;
|
|
27
|
-
}
|
|
28
|
-
if (Number.isFinite(expected.ino) && Number(currentStat.ino) !== Number(expected.ino)) return true;
|
|
29
|
-
return false;
|
|
24
|
+
return !sameFileVersion(currentStat, expected);
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
function atomicWriteShouldFsync(value) {
|
|
@@ -180,7 +175,17 @@ async function assertExclusiveCreate(writeTarget, targetPath, tmp) {
|
|
|
180
175
|
}
|
|
181
176
|
|
|
182
177
|
async function assertTargetUnchanged(writeTarget, expectedTargetSnapshot, tmp) {
|
|
183
|
-
|
|
178
|
+
let currentStat = null;
|
|
179
|
+
try {
|
|
180
|
+
currentStat = statSync(writeTarget);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// A failed lookup is not proof of absence, even for create-only writes.
|
|
183
|
+
if (error?.code !== 'ENOENT') {
|
|
184
|
+
await unlinkQuietly(tmp);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!expectedTargetSnapshotChanged(currentStat, expectedTargetSnapshot)) return;
|
|
184
189
|
await unlinkQuietly(tmp);
|
|
185
190
|
const err = new Error(`target changed between preflight and rename (TOCTOU): ${writeTarget}`);
|
|
186
191
|
err.code = 'ESTALE_TARGET';
|
|
@@ -236,6 +241,11 @@ export async function atomicWrite(
|
|
|
236
241
|
content,
|
|
237
242
|
{ mode, signal, sessionId, flags, fsync, preserveMetadata = false, expectedTargetSnapshot } = {}
|
|
238
243
|
) {
|
|
244
|
+
// Own the caller's expectation before the first await. Missing fields on
|
|
245
|
+
// an existing target remain incomplete evidence, not an unguarded write.
|
|
246
|
+
expectedTargetSnapshot = expectedTargetSnapshot
|
|
247
|
+
? { exists: expectedTargetSnapshot.exists !== false, ...fileVersion(expectedTargetSnapshot) }
|
|
248
|
+
: null;
|
|
239
249
|
const traceStart = ioTraceStart();
|
|
240
250
|
const resolvedSignal = await resolveWriteSignal(signal, sessionId);
|
|
241
251
|
if (resolvedSignal?.aborted) throw abortReasonOf(resolvedSignal);
|
|
@@ -4,6 +4,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
4
4
|
import { isAbsolute, sep } from 'node:path';
|
|
5
5
|
import { canonicalCachePath, deleteReadRangeIndexForPath } from './read-range-index.mjs';
|
|
6
6
|
import { resolveAgainstCwd } from './path-utils.mjs';
|
|
7
|
+
import { fileVersion, sameFileVersion } from './file-version.mjs';
|
|
7
8
|
|
|
8
9
|
const RESULT_CACHE = new Map(); // key → { ts, value, paths, scopes, readSnapshotMeta, contentPrefixHash, bytes }
|
|
9
10
|
const RESULT_CACHE_INFLIGHT = new Map(); // key → { promise, controller, subscribers, settled }
|
|
@@ -45,8 +46,8 @@ function resultCacheDelete(key) {
|
|
|
45
46
|
const STAT_CACHE = new Map(); // fullPath → { ts, stat }
|
|
46
47
|
const STAT_CACHE_TTL_MS = 5_000;
|
|
47
48
|
const STAT_CACHE_MAX_ENTRIES = 2_000;
|
|
48
|
-
const RAW_CONTENT_CACHE = new Map(); // fullPath → { ts,
|
|
49
|
-
const RAW_CONTENT_INFLIGHT = new Map(); //
|
|
49
|
+
const RAW_CONTENT_CACHE = new Map(); // fullPath → { ts, version, rawBuf }
|
|
50
|
+
const RAW_CONTENT_INFLIGHT = new Map(); // path + observed version → { path, promise }
|
|
50
51
|
const RAW_CONTENT_CACHE_TTL_MS = 30_000;
|
|
51
52
|
// 64: a parallel read batch fanning out over a component directory easily
|
|
52
53
|
// tops 16 files; at 16 the batch thrashed its own cache before re-reads hit.
|
|
@@ -232,18 +233,21 @@ async function runPathInFlight(inFlight, key, path, start) {
|
|
|
232
233
|
return await promise;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
export async function runRawContentInFlight(fullPath, loader = fsPromises.readFile) {
|
|
236
|
-
const
|
|
237
|
-
|
|
236
|
+
export async function runRawContentInFlight(fullPath, loader = fsPromises.readFile, stat = undefined) {
|
|
237
|
+
const path = canonicalCachePath(fullPath);
|
|
238
|
+
const version = fileVersion(stat);
|
|
239
|
+
// Buffered reads provide their observed version: a later read must not
|
|
240
|
+
// join older bytes merely because a watcher has not fired yet. Preserve
|
|
241
|
+
// the unversioned helper contract for callers that supply no stat.
|
|
242
|
+
if (stat !== undefined && !version) return await loader(fullPath);
|
|
243
|
+
const key = version ? JSON.stringify([path, version]) : path;
|
|
244
|
+
return await runPathInFlight(RAW_CONTENT_INFLIGHT, key, path, () => loader(fullPath));
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
export async function runReadOnlyStatInFlight(fullPath, loader = fsPromises.stat, kind = 'stat') {
|
|
241
248
|
const canonicalPath = canonicalCachePath(fullPath);
|
|
242
|
-
return await runPathInFlight(
|
|
243
|
-
|
|
244
|
-
`${kind}|${canonicalPath}`,
|
|
245
|
-
canonicalPath,
|
|
246
|
-
() => loader(fullPath)
|
|
249
|
+
return await runPathInFlight(READ_ONLY_STAT_INFLIGHT, `${kind}|${canonicalPath}`, canonicalPath, () =>
|
|
250
|
+
loader(fullPath)
|
|
247
251
|
);
|
|
248
252
|
}
|
|
249
253
|
|
|
@@ -262,11 +266,7 @@ export function rawContentCacheGet(fullPath, stat, now = Date.now()) {
|
|
|
262
266
|
rawContentCacheDelete(key);
|
|
263
267
|
return null;
|
|
264
268
|
}
|
|
265
|
-
if (
|
|
266
|
-
entry.size !== stat.size ||
|
|
267
|
-
Math.abs(entry.mtimeMs - stat.mtimeMs) > 1 ||
|
|
268
|
-
Math.abs(entry.ctimeMs - stat.ctimeMs) > 1
|
|
269
|
-
) {
|
|
269
|
+
if (!sameFileVersion(entry.version, stat)) {
|
|
270
270
|
rawContentCacheDelete(key);
|
|
271
271
|
return null;
|
|
272
272
|
}
|
|
@@ -276,15 +276,14 @@ export function rawContentCacheGet(fullPath, stat, now = Date.now()) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
export function rawContentCacheSet(fullPath, stat, rawBuf, now = Date.now()) {
|
|
279
|
-
if (!fullPath || !
|
|
280
|
-
|
|
279
|
+
if (!fullPath || !Buffer.isBuffer(rawBuf)) return;
|
|
280
|
+
const version = fileVersion(stat);
|
|
281
|
+
if (!version || rawBuf.length !== version.size || rawBuf.length > RAW_CONTENT_CACHE_MAX_BYTES) return;
|
|
281
282
|
const key = canonicalCachePath(fullPath);
|
|
282
283
|
rawContentCacheDelete(key);
|
|
283
284
|
RAW_CONTENT_CACHE.set(key, {
|
|
284
285
|
ts: now,
|
|
285
|
-
|
|
286
|
-
ctimeMs: stat.ctimeMs,
|
|
287
|
-
size: stat.size,
|
|
286
|
+
version,
|
|
288
287
|
rawBuf,
|
|
289
288
|
});
|
|
290
289
|
RAW_CONTENT_CACHE_BYTES += rawBuf.length;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFileSync, mkdirSync, existsSync, lstatSync, realpathSync, statSync } from 'node:fs';
|
|
15
15
|
import { dirname } from 'node:path';
|
|
16
16
|
import { atomicWrite, symlinkWriteTarget } from './atomic-write.mjs';
|
|
17
|
+
import { fileVersion } from './file-version.mjs';
|
|
17
18
|
import { assertPathsReachable } from './fs-reachability.mjs';
|
|
18
19
|
import { normalizeInputPath, resolveAgainstCwd, normalizeOutputPath, toDisplayPath } from './path-utils.mjs';
|
|
19
20
|
import {
|
|
@@ -476,15 +477,7 @@ function readEditTextForDisplay(fullPath, enc) {
|
|
|
476
477
|
const SAME_STRINGS_ERROR = 'Error: old_string and new_string are exactly the same';
|
|
477
478
|
|
|
478
479
|
function targetSnapshot(statBefore) {
|
|
479
|
-
return statBefore
|
|
480
|
-
? {
|
|
481
|
-
exists: true,
|
|
482
|
-
size: statBefore.size,
|
|
483
|
-
mtimeMs: statBefore.mtimeMs,
|
|
484
|
-
ctimeMs: statBefore.ctimeMs,
|
|
485
|
-
ino: statBefore.ino,
|
|
486
|
-
}
|
|
487
|
-
: { exists: false };
|
|
480
|
+
return statBefore ? { exists: true, ...fileVersion(statBefore) } : { exists: false };
|
|
488
481
|
}
|
|
489
482
|
|
|
490
483
|
/**
|
|
@@ -493,12 +486,14 @@ function targetSnapshot(statBefore) {
|
|
|
493
486
|
* Returns `{ error }` when the bytes cannot be read or are not decidable text.
|
|
494
487
|
*/
|
|
495
488
|
function readEditTarget(fullPath) {
|
|
496
|
-
|
|
489
|
+
// An unreadable pre-stat is incomplete existing-file evidence, not the
|
|
490
|
+
// explicit absence used by createEditTarget. Never resurrect a removed file.
|
|
491
|
+
let statBefore = {};
|
|
497
492
|
let content;
|
|
498
493
|
let fileEnc;
|
|
499
494
|
try {
|
|
500
495
|
try {
|
|
501
|
-
statBefore = statSync(fullPath);
|
|
496
|
+
statBefore = { ...fileVersion(statSync(fullPath)) };
|
|
502
497
|
} catch {
|
|
503
498
|
/* readFileSync below surfaces the real error */
|
|
504
499
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
// NTFS identities may exceed Number.MAX_SAFE_INTEGER, where +/- 1 can round
|
|
4
|
+
// back to the original number. Perturb identity, not its precision.
|
|
5
|
+
export function differentFieldValue(current, field, direction = 1) {
|
|
6
|
+
let value;
|
|
7
|
+
if (field === 'ino' || field === 'dev') value = current === 0 ? 1 : 0;
|
|
8
|
+
else value = current + direction * (field.endsWith('Ms') ? 0.5 : 1);
|
|
9
|
+
assert.notEqual(value, current, `${field} fixture must actually differ`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The exact metadata version of a buffered file body. Missing/non-finite
|
|
2
|
+
// evidence is not equality; never round away a detected sub-ms change.
|
|
3
|
+
const FILE_VERSION_FIELDS = ['mtimeMs', 'ctimeMs', 'size', 'ino', 'dev'];
|
|
4
|
+
|
|
5
|
+
export function sameFileVersion(a, b) {
|
|
6
|
+
return !!a && !!b && FILE_VERSION_FIELDS.every((key) => Number.isFinite(a[key]) && a[key] === b[key]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Own the fields rather than retaining a mutable Stats/fixture object.
|
|
10
|
+
export function fileVersion(stat) {
|
|
11
|
+
if (!sameFileVersion(stat, stat)) return null;
|
|
12
|
+
return Object.fromEntries(FILE_VERSION_FIELDS.map((key) => [key, stat[key]]));
|
|
13
|
+
}
|