mixdog 0.9.151 → 0.9.153
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/LICENSES/browser-import-NOTICE.txt +11 -0
- package/NOTICE.md +11 -0
- package/README.md +108 -68
- package/package.json +17 -4
- package/scripts/computer-task-cost-report.mjs +144 -0
- package/scripts/run-office-live-tests.mjs +32 -0
- package/scripts/tool-stress.mjs +0 -2
- package/src/agents/front-worker/AGENT.md +13 -0
- package/src/agents/front-worker/agent.json +6 -0
- package/src/agents/heavy-worker/AGENT.md +10 -11
- package/src/agents/heavy-worker/agent.json +1 -1
- package/src/agents/maintainer/AGENT.md +0 -2
- package/src/agents/reviewer/AGENT.md +9 -13
- package/src/agents/reviewer/agent.json +1 -1
- package/src/agents/security/AGENT.md +14 -0
- package/src/agents/security/agent.json +6 -0
- package/src/agents/worker/AGENT.md +8 -8
- package/src/agents/worker/agent.json +1 -1
- package/src/agents/writer/AGENT.md +14 -0
- package/src/agents/writer/agent.json +6 -0
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/rules/agent/00-common.md +4 -7
- package/src/rules/lead/01-general.md +0 -3
- package/src/rules/shared/10-tool-workflow.md +10 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
- package/src/runtime/agent/orchestrator/config.mjs +13 -68
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
- package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
- package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/pdf-extract.mjs +3 -0
- package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
- package/src/runtime/bridge-clients.test.mjs +827 -0
- package/src/runtime/browser-bridge/action-schema.mjs +251 -0
- package/src/runtime/browser-bridge/client.mjs +152 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
- package/src/runtime/computer-bridge/action-schema.mjs +653 -0
- package/src/runtime/computer-bridge/client.mjs +231 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/office/assurance-benchmark.mjs +358 -0
- package/src/runtime/office/assurance.mjs +833 -0
- package/src/runtime/office/benchmark.mjs +496 -0
- package/src/runtime/office/capabilities.mjs +551 -0
- package/src/runtime/office/com-adapter.mjs +283 -0
- package/src/runtime/office/content-model.mjs +183 -0
- package/src/runtime/office/contract-benchmark.mjs +105 -0
- package/src/runtime/office/design-library.mjs +1562 -0
- package/src/runtime/office/design-system.mjs +1966 -0
- package/src/runtime/office/index.mjs +2413 -0
- package/src/runtime/office/journal.mjs +82 -0
- package/src/runtime/office/office-assurance.test.mjs +371 -0
- package/src/runtime/office/office-com-host.ps1 +3781 -0
- package/src/runtime/office/office-com-session-host.ps1 +855 -0
- package/src/runtime/office/office-live-runtime.test.mjs +819 -0
- package/src/runtime/office/office-runtime.test.mjs +2070 -0
- package/src/runtime/office/ooxml-validator.mjs +271 -0
- package/src/runtime/office/pagination.mjs +132 -0
- package/src/runtime/office/pdf-adapter.mjs +591 -0
- package/src/runtime/office/pdf-analysis.mjs +372 -0
- package/src/runtime/office/pdf-render.mjs +167 -0
- package/src/runtime/office/pdf-security.mjs +63 -0
- package/src/runtime/office/polish-benchmark.mjs +190 -0
- package/src/runtime/office/portable-ooxml.mjs +1867 -0
- package/src/runtime/office/quality-live-benchmark.mjs +486 -0
- package/src/runtime/office/quality-pipeline.mjs +136 -0
- package/src/runtime/office/tabular.mjs +302 -0
- package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
- package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
- package/src/runtime/office/tool-defs.mjs +113 -0
- package/src/runtime/office/visual-diff.mjs +89 -0
- package/src/runtime/office/xlsx-assertions.mjs +172 -0
- package/src/runtime/office/xlsx-contract.mjs +147 -0
- package/src/runtime/shared/agent-route-config.mjs +9 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-execution-contract.mjs +16 -0
- package/src/runtime/shared/tool-surface.mjs +27 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/agent-disable.test.mjs +20 -1
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/deferred-tool-delta.mjs +104 -0
- package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
- package/src/session-runtime/goal-runtime.mjs +1141 -0
- package/src/session-runtime/goal-runtime.test.mjs +645 -0
- package/src/session-runtime/lifecycle-api.mjs +58 -11
- package/src/session-runtime/lifecycle-api.test.mjs +183 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/provider-request-snapshot.mjs +33 -9
- package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
- package/src/session-runtime/resource-api-global.test.mjs +109 -0
- package/src/session-runtime/resource-api.mjs +168 -116
- package/src/session-runtime/runtime-core.mjs +208 -29
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-lifecycle.mjs +34 -12
- package/src/session-runtime/session-title.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +37 -25
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-data.mjs +1 -1
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +42 -112
- package/src/session-runtime/tool-defs.mjs +6 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
- package/src/session-runtime/tool-surface.mjs +30 -8
- package/src/session-runtime/workflow-agents-api.mjs +8 -25
- package/src/session-runtime/workflow.mjs +8 -6
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/job-views.mjs +3 -1
- package/src/standalone/agent-tool/notify.mjs +11 -9
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/agent-tool/render.mjs +3 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
- package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/agent-tool/worker-index.mjs +123 -0
- package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +57 -10
- package/src/standalone/daemon.mjs +113 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +709 -58
- package/src/standalone/session-runtime-inline-host.mjs +258 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +302 -26
- package/src/standalone/session-service-agent-tree.test.mjs +984 -0
- package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
- package/src/standalone/session-service.mjs +711 -11
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +187 -45
- package/src/tui/lib/voice-setup.mjs +4 -1
- package/src/tui/session/agent-message-source.test.mjs +37 -0
- package/src/tui/session/completion-card-restore.test.mjs +50 -0
- package/src/tui/session/context-state.mjs +3 -13
- package/src/tui/session/goal-continuation.mjs +141 -0
- package/src/tui/session/goal-continuation.test.mjs +203 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +190 -0
- package/src/tui/session/session-api-ext.mjs +48 -8
- package/src/tui/session/session-api.mjs +73 -1
- package/src/tui/session/session-flow.mjs +31 -1
- package/src/tui/session/turn.mjs +88 -15
- package/src/tui/session-local.mjs +39 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import test from 'node:test';
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
9
9
|
import { restoreTranscriptItems } from './session-api-ext.mjs';
|
|
10
|
+
import { preserveGoalStateAfterTurn, transcriptToolCallDisplayMode } from './turn.mjs';
|
|
10
11
|
|
|
11
12
|
const wrapper = (taskId, body) => [
|
|
12
13
|
`Async shell task ${taskId} (completed, exit 0) finished.`,
|
|
@@ -109,3 +110,52 @@ test('task wait calls stay hidden when a session transcript is restored', () =>
|
|
|
109
110
|
assert.equal(items.some((item) => item?.kind === 'tool' && item?.args?.action === 'wait'), false);
|
|
110
111
|
assert.equal(items.some((item) => item?.kind === 'tool' && item?.args?.action === 'read'), true);
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
test('Goal and load control calls never enter restored tool aggregates', () => {
|
|
115
|
+
const hiddenNames = [
|
|
116
|
+
'goal',
|
|
117
|
+
'create_goal',
|
|
118
|
+
'get_goal',
|
|
119
|
+
'set_goal_tasks',
|
|
120
|
+
'update_goal',
|
|
121
|
+
'load_tool',
|
|
122
|
+
'tool_search',
|
|
123
|
+
'Skill',
|
|
124
|
+
];
|
|
125
|
+
const toolCalls = hiddenNames.map((name, index) => ({
|
|
126
|
+
id: `call_hidden_${index}`,
|
|
127
|
+
name,
|
|
128
|
+
arguments: { action: 'status' },
|
|
129
|
+
}));
|
|
130
|
+
toolCalls.push({ id: 'call_read', name: 'read', arguments: { file_path: 'visible.txt' } });
|
|
131
|
+
const messages = [
|
|
132
|
+
{ role: 'assistant', toolCalls },
|
|
133
|
+
...toolCalls.map((call) => ({
|
|
134
|
+
role: 'tool',
|
|
135
|
+
toolCallId: call.id,
|
|
136
|
+
content: 'ok',
|
|
137
|
+
})),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const items = restoreTranscriptItems(messages, { sessionId: 'sess_hidden_controls_restore' });
|
|
141
|
+
const toolItems = items.filter((item) => item?.kind === 'tool');
|
|
142
|
+
assert.equal(toolItems.length, 1);
|
|
143
|
+
assert.equal(toolItems[0].name, 'read');
|
|
144
|
+
assert.equal(toolItems.some((item) =>
|
|
145
|
+
(item.toolMembers || []).some((member) => hiddenNames.includes(member?.name))), false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('live turn display policy suppresses Goal/load controls without hiding ordinary tools', () => {
|
|
149
|
+
assert.equal(transcriptToolCallDisplayMode('goal', { action: 'create' }), 'hidden-control');
|
|
150
|
+
assert.equal(transcriptToolCallDisplayMode('functions.load_tool', { name: 'browser' }), 'hidden-control');
|
|
151
|
+
assert.equal(transcriptToolCallDisplayMode('task', { action: 'wait' }), 'task-wait');
|
|
152
|
+
assert.equal(transcriptToolCallDisplayMode('read', { file_path: 'visible.txt' }), 'visible');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('surface replacement preserves Goal state while an explicit user cancellation still pauses', () => {
|
|
156
|
+
assert.equal(preserveGoalStateAfterTurn({ cancelled: true, stale: true }), true);
|
|
157
|
+
assert.equal(preserveGoalStateAfterTurn({ cancelled: true, pendingSessionReset: true }), true);
|
|
158
|
+
assert.equal(preserveGoalStateAfterTurn({ cancelled: true, disposed: true }), true);
|
|
159
|
+
assert.equal(preserveGoalStateAfterTurn({ cancelled: true }), false);
|
|
160
|
+
assert.equal(preserveGoalStateAfterTurn({ cancelled: false, stale: true }), false);
|
|
161
|
+
});
|
|
@@ -48,6 +48,7 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
48
48
|
webSearchRoute: runtime.getWebSearchRoute?.() || runtime.webSearchRoute || null,
|
|
49
49
|
autoClear: autoClearState(),
|
|
50
50
|
workflow: runtime.workflow || null,
|
|
51
|
+
goal: runtime.goalStatus?.() || null,
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
const routeState = () => {
|
|
@@ -93,7 +94,6 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
93
94
|
const syncContextStats = ({
|
|
94
95
|
allowEstimated = false,
|
|
95
96
|
invalidateExact = false,
|
|
96
|
-
compactedEstimateTokens = null,
|
|
97
97
|
} = {}) => {
|
|
98
98
|
if (getPendingSessionReset()) return null;
|
|
99
99
|
const ctx = runtime.contextStatus?.() || null;
|
|
@@ -117,24 +117,14 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
117
117
|
updateState({ stats });
|
|
118
118
|
return ctx;
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
// replacement transcript. Prefer it for this publication instead of
|
|
122
|
-
// letting a lagging contextStatus/provider snapshot repaint the deleted
|
|
123
|
-
// pre-compact prefix until the next request lands.
|
|
124
|
-
const compactedEstimate = Number(compactedEstimateTokens);
|
|
125
|
-
const hasCompactedEstimate = compactedEstimateTokens != null
|
|
126
|
-
&& Number.isFinite(compactedEstimate)
|
|
127
|
-
&& compactedEstimate > 0;
|
|
128
|
-
const estimatedTokens = hasCompactedEstimate
|
|
129
|
-
? compactedEstimate
|
|
130
|
-
: Math.max(0, Number(ctx.currentEstimatedTokens ?? ctx.usedTokens ?? 0));
|
|
120
|
+
const estimatedTokens = Math.max(0, Number(ctx.currentEstimatedTokens ?? ctx.usedTokens ?? 0));
|
|
131
121
|
const usedTokens = Math.max(0, Number(ctx.usedTokens ?? estimatedTokens ?? 0));
|
|
132
122
|
const usedSource = String(ctx.usedSource || '').toLowerCase();
|
|
133
123
|
// A successful pre-send auto-compact rewrites the transcript before the
|
|
134
124
|
// next provider request can publish fresh usage. Never keep painting the
|
|
135
125
|
// exact token count measured against the deleted pre-compact prefix during
|
|
136
126
|
// that gap; publish the compacted transcript estimate immediately.
|
|
137
|
-
const forceEstimated =
|
|
127
|
+
const forceEstimated = invalidateExact && estimatedTokens > 0;
|
|
138
128
|
const shouldPublishEstimate = allowEstimated && (
|
|
139
129
|
usedSource === 'estimated'
|
|
140
130
|
|| Number(ctx.currentEstimatedTokens) > 0
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
function clean(value) {
|
|
2
|
+
return String(value ?? '').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isGoalContinuation(entry) {
|
|
6
|
+
return entry?.mode === 'goal-continuation';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createGoalContinuation({
|
|
10
|
+
runtime,
|
|
11
|
+
flags,
|
|
12
|
+
getState,
|
|
13
|
+
set,
|
|
14
|
+
getPending,
|
|
15
|
+
enqueue,
|
|
16
|
+
} = {}) {
|
|
17
|
+
let scheduled = null;
|
|
18
|
+
let disposed = false;
|
|
19
|
+
let suppressedCompletedGoalId = '';
|
|
20
|
+
|
|
21
|
+
const cancelQueuedGoalContinuations = () => {
|
|
22
|
+
const pending = getPending();
|
|
23
|
+
let removed = 0;
|
|
24
|
+
for (let index = pending.length - 1; index >= 0; index -= 1) {
|
|
25
|
+
if (!isGoalContinuation(pending[index])) continue;
|
|
26
|
+
pending.splice(index, 1);
|
|
27
|
+
removed += 1;
|
|
28
|
+
}
|
|
29
|
+
return removed;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const visibleGoal = (goal) => (
|
|
33
|
+
goal?.status === 'complete' && clean(goal.id) === suppressedCompletedGoalId
|
|
34
|
+
? null
|
|
35
|
+
: goal || null
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const refreshGoalState = () => {
|
|
39
|
+
const goal = visibleGoal(runtime.goalStatus?.() || null);
|
|
40
|
+
if (getState().goal !== goal) set({ goal });
|
|
41
|
+
return goal;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const continuationDecision = () => {
|
|
45
|
+
const decision = runtime.goalContinuation?.() || { run: false, reason: 'unavailable', goal: null };
|
|
46
|
+
if (getState().goal !== decision.goal) set({ goal: decision.goal || null });
|
|
47
|
+
return decision;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const shouldRunGoalContinuation = (entry) => {
|
|
51
|
+
if (disposed || flags.disposed || flags.pendingSessionReset) return false;
|
|
52
|
+
if (getState().sessionRemoteAttached) return false;
|
|
53
|
+
const decision = continuationDecision();
|
|
54
|
+
if (!decision.run) return false;
|
|
55
|
+
const queuedGoalId = clean(entry?.goalId);
|
|
56
|
+
return !queuedGoalId || queuedGoalId === clean(decision.goal?.id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const scheduleGoalContinuation = () => {
|
|
60
|
+
if (disposed || scheduled) return false;
|
|
61
|
+
scheduled = setImmediate(() => {
|
|
62
|
+
scheduled = null;
|
|
63
|
+
if (disposed || flags.disposed || flags.pendingSessionReset) return;
|
|
64
|
+
const state = getState();
|
|
65
|
+
if (state.busy || state.commandBusy || state.sessionRemoteAttached) return;
|
|
66
|
+
const pending = getPending();
|
|
67
|
+
if (pending.some((entry) => !isGoalContinuation(entry))) return;
|
|
68
|
+
if (pending.some(isGoalContinuation)) return;
|
|
69
|
+
const decision = continuationDecision();
|
|
70
|
+
if (!decision.run || !clean(decision.prompt)) return;
|
|
71
|
+
enqueue(decision.prompt, {
|
|
72
|
+
mode: 'goal-continuation',
|
|
73
|
+
priority: 'later',
|
|
74
|
+
isMeta: true,
|
|
75
|
+
suppressDisplay: true,
|
|
76
|
+
skipSlashCommands: true,
|
|
77
|
+
restorable: false,
|
|
78
|
+
goalId: clean(decision.goal?.id),
|
|
79
|
+
displayText: '',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
scheduled.unref?.();
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onGoalChanged = (event = {}) => {
|
|
87
|
+
const currentSessionId = clean(getState().sessionId || runtime.id);
|
|
88
|
+
if (clean(event.sessionId) && clean(event.sessionId) !== currentSessionId) return;
|
|
89
|
+
cancelQueuedGoalContinuations();
|
|
90
|
+
const goal = visibleGoal(event.goal || runtime.goalStatus?.() || null);
|
|
91
|
+
set({ goal });
|
|
92
|
+
if (goal?.status === 'active') scheduleGoalContinuation();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const unsubscribe = runtime.onGoalStatusChange?.(onGoalChanged) || (() => {});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
cancelQueuedGoalContinuations,
|
|
99
|
+
refreshGoalState,
|
|
100
|
+
scheduleGoalContinuation,
|
|
101
|
+
shouldRunGoalContinuation,
|
|
102
|
+
async onGoalTurnStarted() {
|
|
103
|
+
const goal = await Promise.resolve(runtime.goalTurnStarted?.());
|
|
104
|
+
if (goal !== undefined && getState().goal !== goal) set({ goal: goal || null });
|
|
105
|
+
return goal;
|
|
106
|
+
},
|
|
107
|
+
async onGoalTurnSettled(detail = {}) {
|
|
108
|
+
const status = clean(typeof detail === 'string' ? detail : detail.status).toLowerCase();
|
|
109
|
+
const goal = await Promise.resolve(runtime.goalTurnSettled?.(
|
|
110
|
+
typeof detail === 'string' ? { status } : detail,
|
|
111
|
+
));
|
|
112
|
+
if (goal !== undefined && getState().goal !== goal) set({ goal: goal || null });
|
|
113
|
+
if (status === 'done' && goal?.status === 'active') scheduleGoalContinuation();
|
|
114
|
+
return goal;
|
|
115
|
+
},
|
|
116
|
+
archiveCompletedGoalOnUserInput() {
|
|
117
|
+
cancelQueuedGoalContinuations();
|
|
118
|
+
const currentGoal = getState().goal || runtime.goalStatus?.() || null;
|
|
119
|
+
const archivedGoalId = currentGoal?.status === 'complete' ? clean(currentGoal.id) : '';
|
|
120
|
+
if (archivedGoalId) {
|
|
121
|
+
suppressedCompletedGoalId = archivedGoalId;
|
|
122
|
+
set({ goal: null });
|
|
123
|
+
}
|
|
124
|
+
void Promise.resolve(runtime.archiveCompletedGoalOnUserInput?.())
|
|
125
|
+
.then(() => {
|
|
126
|
+
if (suppressedCompletedGoalId === archivedGoalId) suppressedCompletedGoalId = '';
|
|
127
|
+
refreshGoalState();
|
|
128
|
+
})
|
|
129
|
+
.catch(() => {
|
|
130
|
+
if (suppressedCompletedGoalId === archivedGoalId) suppressedCompletedGoalId = '';
|
|
131
|
+
refreshGoalState();
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
disposeGoalContinuation() {
|
|
135
|
+
disposed = true;
|
|
136
|
+
if (scheduled) clearImmediate(scheduled);
|
|
137
|
+
scheduled = null;
|
|
138
|
+
try { unsubscribe(); } catch {}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { createGoalContinuation } from './goal-continuation.mjs';
|
|
5
|
+
|
|
6
|
+
const settleImmediate = () => new Promise((resolve) => setImmediate(resolve));
|
|
7
|
+
|
|
8
|
+
test('completed Goal stays hidden while its user-input archive is in flight', async () => {
|
|
9
|
+
const goal = { id: 'goal-complete', status: 'complete', objective: 'Finished work' };
|
|
10
|
+
const state = {
|
|
11
|
+
busy: false,
|
|
12
|
+
commandBusy: false,
|
|
13
|
+
sessionId: 'sess_goal_complete',
|
|
14
|
+
goal,
|
|
15
|
+
};
|
|
16
|
+
let runtimeGoal = goal;
|
|
17
|
+
let listener = null;
|
|
18
|
+
let resolveArchive;
|
|
19
|
+
const archive = new Promise((resolve) => { resolveArchive = resolve; });
|
|
20
|
+
const controller = createGoalContinuation({
|
|
21
|
+
runtime: {
|
|
22
|
+
id: state.sessionId,
|
|
23
|
+
goalStatus: () => runtimeGoal,
|
|
24
|
+
archiveCompletedGoalOnUserInput: () => archive,
|
|
25
|
+
onGoalStatusChange: (next) => {
|
|
26
|
+
listener = next;
|
|
27
|
+
return () => { listener = null; };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
flags: { disposed: false, pendingSessionReset: false },
|
|
31
|
+
getState: () => state,
|
|
32
|
+
set: (patch) => Object.assign(state, patch),
|
|
33
|
+
getPending: () => [],
|
|
34
|
+
enqueue: () => true,
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
controller.archiveCompletedGoalOnUserInput();
|
|
38
|
+
assert.equal(state.goal, null);
|
|
39
|
+
|
|
40
|
+
listener?.({ sessionId: state.sessionId, goal });
|
|
41
|
+
assert.equal(state.goal, null);
|
|
42
|
+
|
|
43
|
+
runtimeGoal = null;
|
|
44
|
+
resolveArchive(null);
|
|
45
|
+
await archive;
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
assert.equal(state.goal, null);
|
|
48
|
+
} finally {
|
|
49
|
+
controller.disposeGoalContinuation();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('failed completed Goal archive restores the persisted Goal snapshot', async () => {
|
|
54
|
+
const goal = { id: 'goal-restore', status: 'complete', objective: 'Restore me' };
|
|
55
|
+
const state = {
|
|
56
|
+
busy: false,
|
|
57
|
+
commandBusy: false,
|
|
58
|
+
sessionId: 'sess_goal_restore',
|
|
59
|
+
goal,
|
|
60
|
+
};
|
|
61
|
+
let rejectArchive;
|
|
62
|
+
const archive = new Promise((_resolve, reject) => { rejectArchive = reject; });
|
|
63
|
+
const controller = createGoalContinuation({
|
|
64
|
+
runtime: {
|
|
65
|
+
id: state.sessionId,
|
|
66
|
+
goalStatus: () => goal,
|
|
67
|
+
archiveCompletedGoalOnUserInput: () => archive,
|
|
68
|
+
onGoalStatusChange: () => () => {},
|
|
69
|
+
},
|
|
70
|
+
flags: { disposed: false, pendingSessionReset: false },
|
|
71
|
+
getState: () => state,
|
|
72
|
+
set: (patch) => Object.assign(state, patch),
|
|
73
|
+
getPending: () => [],
|
|
74
|
+
enqueue: () => true,
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
controller.archiveCompletedGoalOnUserInput();
|
|
78
|
+
assert.equal(state.goal, null);
|
|
79
|
+
|
|
80
|
+
rejectArchive(new Error('disk write failed'));
|
|
81
|
+
await archive.catch(() => {});
|
|
82
|
+
await Promise.resolve();
|
|
83
|
+
assert.equal(state.goal, goal);
|
|
84
|
+
} finally {
|
|
85
|
+
controller.disposeGoalContinuation();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('idle Goal schedules one hidden continuation and user input cancels it', async () => {
|
|
90
|
+
const goal = { id: 'goal-1', status: 'active', objective: 'Finish it' };
|
|
91
|
+
const state = { busy: false, commandBusy: false, sessionId: 'sess_goal', goal };
|
|
92
|
+
const pending = [];
|
|
93
|
+
let listener = null;
|
|
94
|
+
const controller = createGoalContinuation({
|
|
95
|
+
runtime: {
|
|
96
|
+
id: 'sess_goal',
|
|
97
|
+
goalStatus: () => goal,
|
|
98
|
+
goalContinuation: () => ({ run: true, reason: 'idle', goal, prompt: 'hidden continuation' }),
|
|
99
|
+
goalTurnStarted: async () => goal,
|
|
100
|
+
goalTurnSettled: async () => goal,
|
|
101
|
+
onGoalStatusChange: (next) => {
|
|
102
|
+
listener = next;
|
|
103
|
+
return () => { listener = null; };
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
flags: { disposed: false, pendingSessionReset: false },
|
|
107
|
+
getState: () => state,
|
|
108
|
+
set: (patch) => Object.assign(state, patch),
|
|
109
|
+
getPending: () => pending,
|
|
110
|
+
enqueue: (text, options) => {
|
|
111
|
+
pending.push({ content: text, ...options });
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
assert.equal(controller.scheduleGoalContinuation(), true);
|
|
117
|
+
controller.scheduleGoalContinuation();
|
|
118
|
+
await settleImmediate();
|
|
119
|
+
assert.equal(pending.length, 1);
|
|
120
|
+
assert.equal(pending[0].mode, 'goal-continuation');
|
|
121
|
+
assert.equal(pending[0].priority, 'later');
|
|
122
|
+
assert.equal(controller.shouldRunGoalContinuation(pending[0]), true);
|
|
123
|
+
|
|
124
|
+
controller.cancelQueuedGoalContinuations();
|
|
125
|
+
assert.equal(pending.length, 0);
|
|
126
|
+
|
|
127
|
+
listener?.({ sessionId: 'sess_goal', goal: { ...goal, status: 'paused' } });
|
|
128
|
+
await settleImmediate();
|
|
129
|
+
assert.equal(pending.length, 0);
|
|
130
|
+
} finally {
|
|
131
|
+
controller.disposeGoalContinuation();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('busy or agent-parked Goal does not enqueue a continuation', async () => {
|
|
136
|
+
const goal = { id: 'goal-2', status: 'active' };
|
|
137
|
+
const state = { busy: true, commandBusy: false, sessionId: 'sess_goal_busy', goal };
|
|
138
|
+
const pending = [];
|
|
139
|
+
const controller = createGoalContinuation({
|
|
140
|
+
runtime: {
|
|
141
|
+
id: 'sess_goal_busy',
|
|
142
|
+
goalStatus: () => goal,
|
|
143
|
+
goalContinuation: () => ({ run: false, reason: 'agent-running', goal }),
|
|
144
|
+
onGoalStatusChange: () => () => {},
|
|
145
|
+
},
|
|
146
|
+
flags: { disposed: false, pendingSessionReset: false },
|
|
147
|
+
getState: () => state,
|
|
148
|
+
set: (patch) => Object.assign(state, patch),
|
|
149
|
+
getPending: () => pending,
|
|
150
|
+
enqueue: (text, options) => pending.push({ content: text, ...options }),
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
controller.scheduleGoalContinuation();
|
|
154
|
+
await settleImmediate();
|
|
155
|
+
assert.equal(pending.length, 0);
|
|
156
|
+
state.busy = false;
|
|
157
|
+
await controller.onGoalTurnSettled({ status: 'done' });
|
|
158
|
+
await settleImmediate();
|
|
159
|
+
assert.equal(pending.length, 0);
|
|
160
|
+
} finally {
|
|
161
|
+
controller.disposeGoalContinuation();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('restart hydration preserves every non-active Goal snapshot without scheduling work', async () => {
|
|
166
|
+
for (const status of ['paused', 'blocked', 'complete']) {
|
|
167
|
+
const goal = {
|
|
168
|
+
id: `goal-${status}`,
|
|
169
|
+
status,
|
|
170
|
+
objective: `Keep ${status}`,
|
|
171
|
+
tasks: [{ id: 'task_1', text: 'Keep progress', status: 'completed', kind: 'work' }],
|
|
172
|
+
};
|
|
173
|
+
const state = {
|
|
174
|
+
busy: false,
|
|
175
|
+
commandBusy: false,
|
|
176
|
+
sessionId: `sess_goal_${status}`,
|
|
177
|
+
goal: null,
|
|
178
|
+
};
|
|
179
|
+
const pending = [];
|
|
180
|
+
const controller = createGoalContinuation({
|
|
181
|
+
runtime: {
|
|
182
|
+
id: state.sessionId,
|
|
183
|
+
goalStatus: () => goal,
|
|
184
|
+
goalContinuation: () => ({ run: false, reason: status, goal }),
|
|
185
|
+
onGoalStatusChange: () => () => {},
|
|
186
|
+
},
|
|
187
|
+
flags: { disposed: false, pendingSessionReset: false },
|
|
188
|
+
getState: () => state,
|
|
189
|
+
set: (patch) => Object.assign(state, patch),
|
|
190
|
+
getPending: () => pending,
|
|
191
|
+
enqueue: (text, options) => pending.push({ content: text, ...options }),
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
assert.equal(controller.refreshGoalState(), goal);
|
|
195
|
+
assert.equal(state.goal, goal);
|
|
196
|
+
controller.scheduleGoalContinuation();
|
|
197
|
+
await settleImmediate();
|
|
198
|
+
assert.equal(pending.length, 0);
|
|
199
|
+
} finally {
|
|
200
|
+
controller.disposeGoalContinuation();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
@@ -36,7 +36,10 @@ export function defaultQueuePriority(mode) {
|
|
|
36
36
|
|
|
37
37
|
export function isQueuedEntryEditable(entry) {
|
|
38
38
|
const mode = entry?.mode || 'prompt';
|
|
39
|
-
return mode !== 'task-notification'
|
|
39
|
+
return mode !== 'task-notification'
|
|
40
|
+
&& mode !== 'pending-resume'
|
|
41
|
+
&& mode !== 'goal-continuation'
|
|
42
|
+
&& entry?.isMeta !== true;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export function isQueuedEntryVisible(entry) {
|
|
@@ -44,7 +47,7 @@ export function isQueuedEntryVisible(entry) {
|
|
|
44
47
|
// task completions stay in the internal pending queue, but should never look
|
|
45
48
|
// like commands typed by the user while they wait to be drained.
|
|
46
49
|
const mode = entry?.mode || 'prompt';
|
|
47
|
-
if (mode === 'pending-resume') return false;
|
|
50
|
+
if (mode === 'pending-resume' || mode === 'goal-continuation') return false;
|
|
48
51
|
return isQueuedEntryEditable(entry);
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { createSessionApi } from './session-api.mjs';
|
|
5
|
+
import {
|
|
6
|
+
SESSION_CONFIGURE_ACTIONS,
|
|
7
|
+
SESSION_READ_ACTIONS,
|
|
8
|
+
} from '../../standalone/session-protocol.mjs';
|
|
9
|
+
|
|
10
|
+
// The daemon resolves every session action by NAME on the object
|
|
11
|
+
// createLocalSessionRuntime returns — this surface. An action that only exists
|
|
12
|
+
// on the runtime beneath it passes the protocol allowlist and then dies in the
|
|
13
|
+
// session runtime worker as "session action <name> is unavailable" (desktop
|
|
14
|
+
// /inherit shipped exactly that way).
|
|
15
|
+
function stubBag(overrides = {}) {
|
|
16
|
+
const defaults = {
|
|
17
|
+
flags: {},
|
|
18
|
+
autoClearState: {},
|
|
19
|
+
pending: [],
|
|
20
|
+
listeners: new Set(),
|
|
21
|
+
};
|
|
22
|
+
return new Proxy({}, {
|
|
23
|
+
get: (_target, key) => {
|
|
24
|
+
if (key in overrides) return overrides[key];
|
|
25
|
+
if (key in defaults) return defaults[key];
|
|
26
|
+
return () => ({});
|
|
27
|
+
},
|
|
28
|
+
has: () => true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('every advertised session action exists on the session surface', () => {
|
|
33
|
+
const api = createSessionApi(stubBag());
|
|
34
|
+
const missing = [...SESSION_READ_ACTIONS, ...SESSION_CONFIGURE_ACTIONS]
|
|
35
|
+
.filter((action) => typeof api[action] !== 'function');
|
|
36
|
+
assert.deepEqual(missing, []);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('accepted user input archives completed Goal UI before the next turn', () => {
|
|
40
|
+
const calls = [];
|
|
41
|
+
const api = createSessionApi(stubBag({
|
|
42
|
+
nextId: () => 'submission-goal-archive',
|
|
43
|
+
getState: () => ({ commandBusy: false, busy: true }),
|
|
44
|
+
enqueue: () => {
|
|
45
|
+
calls.push('enqueue');
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
cancelQueuedGoalContinuations: () => calls.push('cancel-goal-continuation'),
|
|
49
|
+
archiveCompletedGoalOnUserInput: () => calls.push('archive-completed-goal'),
|
|
50
|
+
runtime: { interruptTaskWait() {} },
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
assert.equal(api.submit('next request'), true);
|
|
54
|
+
assert.deepEqual(calls, [
|
|
55
|
+
'enqueue',
|
|
56
|
+
'cancel-goal-continuation',
|
|
57
|
+
'archive-completed-goal',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('submitAndWait resolves with the main turn settlement', async () => {
|
|
62
|
+
let state = { commandBusy: false, busy: false };
|
|
63
|
+
const api = createSessionApi(stubBag({
|
|
64
|
+
nextId: () => 'submission-1',
|
|
65
|
+
getState: () => state,
|
|
66
|
+
set: (patch) => { state = { ...state, ...patch }; },
|
|
67
|
+
autoClearBeforeSubmit: async () => {},
|
|
68
|
+
enqueue: (_text, options) => {
|
|
69
|
+
queueMicrotask(() => options.onSettled({
|
|
70
|
+
status: 'done',
|
|
71
|
+
result: { content: 'answer' },
|
|
72
|
+
session: { id: 'sess_agent' },
|
|
73
|
+
}));
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const settled = await api.submitAndWait('question');
|
|
79
|
+
assert.equal(settled.status, 'done');
|
|
80
|
+
assert.equal(settled.result.content, 'answer');
|
|
81
|
+
assert.equal(settled.session.id, 'sess_agent');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('canonical child close is exposed on the session surface', () => {
|
|
85
|
+
const calls = [];
|
|
86
|
+
const api = createSessionApi(stubBag({
|
|
87
|
+
runtime: {
|
|
88
|
+
closeCanonicalSession(reason) {
|
|
89
|
+
calls.push(reason);
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
assert.equal(typeof api.closeCanonicalSession, 'function');
|
|
96
|
+
assert.equal(api.closeCanonicalSession('cli-agent-close'), true);
|
|
97
|
+
assert.deepEqual(calls, ['cli-agent-close']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('canonical Agent completion is delivered through the session surface', () => {
|
|
101
|
+
const calls = [];
|
|
102
|
+
const api = createSessionApi(stubBag({
|
|
103
|
+
runtime: {
|
|
104
|
+
deliverToolCompletion(sessionId, text, meta) {
|
|
105
|
+
calls.push({ sessionId, text, meta });
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}));
|
|
110
|
+
const meta = { type: 'agent_task_result', execution_id: 'task-agent-1' };
|
|
111
|
+
|
|
112
|
+
assert.equal(typeof api.deliverToolCompletion, 'function');
|
|
113
|
+
assert.equal(api.deliverToolCompletion('sess_lead', 'Agent handoff', meta), true);
|
|
114
|
+
assert.deepEqual(calls, [{
|
|
115
|
+
sessionId: 'sess_lead',
|
|
116
|
+
text: 'Agent handoff',
|
|
117
|
+
meta,
|
|
118
|
+
}]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('extension editor actions are advertised and forwarded', async () => {
|
|
122
|
+
const calls = [];
|
|
123
|
+
let state = { commandBusy: false, stats: {} };
|
|
124
|
+
const api = createSessionApi(stubBag({
|
|
125
|
+
runtime: {
|
|
126
|
+
getMcpServerConfig: (name) => {
|
|
127
|
+
calls.push(['getMcpServerConfig', name]);
|
|
128
|
+
return { name };
|
|
129
|
+
},
|
|
130
|
+
saveMcpServer: async (input) => {
|
|
131
|
+
calls.push(['saveMcpServer', input]);
|
|
132
|
+
return { name: input.name };
|
|
133
|
+
},
|
|
134
|
+
saveSkill: async (input) => {
|
|
135
|
+
calls.push(['saveSkill', input]);
|
|
136
|
+
return { skill: { name: input.name } };
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
getState: () => state,
|
|
140
|
+
set: (patch) => { state = { ...state, ...patch }; },
|
|
141
|
+
routeState: () => ({}),
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
assert.ok(SESSION_READ_ACTIONS.includes('getMcpServerConfig'));
|
|
145
|
+
assert.ok(SESSION_CONFIGURE_ACTIONS.includes('saveMcpServer'));
|
|
146
|
+
assert.ok(SESSION_CONFIGURE_ACTIONS.includes('saveSkill'));
|
|
147
|
+
assert.deepEqual(api.getMcpServerConfig('UnityMCP'), { name: 'UnityMCP' });
|
|
148
|
+
await api.saveMcpServer({ name: 'UnityMCP' });
|
|
149
|
+
await api.saveSkill({ name: 'unity-helper' });
|
|
150
|
+
assert.deepEqual(calls, [
|
|
151
|
+
['getMcpServerConfig', 'UnityMCP'],
|
|
152
|
+
['saveMcpServer', { name: 'UnityMCP' }],
|
|
153
|
+
['saveSkill', { name: 'unity-helper' }],
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('inheritFrom carries the conversation and rebuilds the heir transcript', async () => {
|
|
158
|
+
const carried = [
|
|
159
|
+
{ role: 'system', content: 'target system prompt' },
|
|
160
|
+
{ role: 'user', content: 'first question' },
|
|
161
|
+
{ role: 'assistant', content: 'first answer' },
|
|
162
|
+
];
|
|
163
|
+
let state = { sessionId: 'sess_heir', stats: {}, items: [] };
|
|
164
|
+
const calls = [];
|
|
165
|
+
const api = createSessionApi(stubBag({
|
|
166
|
+
runtime: {
|
|
167
|
+
inheritFrom: (sourceSessionId) => {
|
|
168
|
+
calls.push(sourceSessionId);
|
|
169
|
+
return { sessionId: 'sess_heir', sourceSessionId, messages: 2 };
|
|
170
|
+
},
|
|
171
|
+
readModelMessages: () => ({ messageCount: carried.length, messages: carried }),
|
|
172
|
+
},
|
|
173
|
+
getState: () => state,
|
|
174
|
+
set: (patch) => { state = { ...state, ...patch }; },
|
|
175
|
+
replaceItems: (items) => items,
|
|
176
|
+
routeState: () => ({}),
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
const result = await api.inheritFrom('sess_source');
|
|
180
|
+
|
|
181
|
+
assert.deepEqual(calls, ['sess_source']);
|
|
182
|
+
assert.equal(result.messages, 2);
|
|
183
|
+
assert.equal(state.sessionId, 'sess_heir');
|
|
184
|
+
// Only the conversation travels: the target's own system block never becomes
|
|
185
|
+
// a transcript item.
|
|
186
|
+
assert.deepEqual(
|
|
187
|
+
state.items.map((item) => [item.kind, item.text]),
|
|
188
|
+
[['user', 'first question'], ['assistant', 'first answer']],
|
|
189
|
+
);
|
|
190
|
+
});
|