mixdog 0.9.152 → 0.9.154
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 +8 -10
- package/package.json +19 -3
- package/scripts/computer-task-cost-report.mjs +144 -0
- package/scripts/run-office-live-tests.mjs +32 -0
- 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/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/rules/shared/60-verification.md +2 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
- 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/config.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -10
- package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +140 -86
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -13
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +72 -4
- package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +0 -22
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +11 -23
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +28 -269
- package/src/runtime/agent/orchestrator/session/compact.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +86 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
- 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/recall-fasttrack.mjs +80 -18
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +80 -21
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +33 -72
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -51
- 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 +39 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +2 -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-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-visibility.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +4 -0
- package/src/runtime/attachments/pdf-extract.mjs +3 -0
- package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
- package/src/runtime/browser-bridge/action-schema.mjs +251 -0
- package/src/runtime/browser-bridge/client.mjs +75 -26
- package/src/runtime/browser-bridge/tool-defs.mjs +132 -15
- package/src/runtime/computer-bridge/action-schema.mjs +653 -0
- package/src/runtime/computer-bridge/client.mjs +211 -28
- package/src/runtime/computer-bridge/tool-defs.mjs +25 -45
- 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-com-host.ps1 +3781 -0
- package/src/runtime/office/office-com-session-host.ps1 +855 -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/tool-execution-contract.mjs +16 -0
- package/src/runtime/shared/tool-surface.mjs +9 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/deferred-tool-delta.mjs +104 -0
- package/src/session-runtime/goal-runtime.mjs +617 -197
- package/src/session-runtime/lifecycle-api.mjs +50 -8
- package/src/session-runtime/provider-request-snapshot.mjs +33 -9
- package/src/session-runtime/resource-api.mjs +31 -94
- package/src/session-runtime/runtime-core.mjs +78 -26
- 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 +15 -14
- package/src/session-runtime/tool-catalog-data.mjs +1 -1
- package/src/session-runtime/tool-catalog.mjs +38 -99
- package/src/session-runtime/tool-defs.mjs +6 -1
- package/src/session-runtime/tool-surface.mjs +18 -8
- package/src/session-runtime/workflow-agents-api.mjs +8 -25
- package/src/session-runtime/workflow.mjs +8 -6
- 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 +5 -1
- package/src/standalone/agent-tool/render.mjs +3 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
- 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-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +57 -10
- package/src/standalone/daemon.mjs +105 -6
- package/src/standalone/session-runtime-host.mjs +61 -48
- package/src/standalone/session-runtime-inline-host.mjs +34 -127
- package/src/standalone/session-runtime-worker.mjs +20 -198
- package/src/standalone/session-service.mjs +632 -7
- package/src/tui/dist/index.mjs +24 -1
- package/src/tui/lib/voice-setup.mjs +4 -1
- package/src/tui/session/context-state.mjs +2 -13
- package/src/tui/session/goal-continuation.mjs +38 -7
- package/src/tui/session/session-api-ext.mjs +9 -0
- package/src/tui/session/session-api.mjs +15 -1
- package/src/tui/session/session-flow.mjs +25 -1
- package/src/tui/session/turn.mjs +88 -16
- package/src/tui/session-local.mjs +25 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +0 -135
- package/scripts/native-binary-arch.test.mjs +0 -111
- package/scripts/patch-replay.test.mjs +0 -94
- package/src/headless-exec.test.mjs +0 -539
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +0 -57
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +0 -153
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +0 -139
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +0 -66
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +0 -80
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +0 -90
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +0 -215
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.test.mjs +0 -165
- package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +0 -142
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -153
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.test.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +0 -190
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +0 -235
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +0 -719
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +0 -40
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +0 -176
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +0 -161
- package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +0 -115
- package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +0 -228
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +0 -245
- package/src/runtime/agent/orchestrator/session/lossless-tool-output.test.mjs +0 -125
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +0 -325
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +0 -70
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +0 -457
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.test.mjs +0 -117
- package/src/runtime/agent/orchestrator/session/reduction-metrics.test.mjs +0 -87
- package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +0 -60
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +0 -583
- package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +0 -78
- package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +0 -43
- package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +0 -63
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +0 -14
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +0 -224
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +0 -224
- package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +0 -56
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +0 -103
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.test.mjs +0 -30
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +0 -390
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.test.mjs +0 -83
- package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +0 -31
- package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +0 -106
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.test.mjs +0 -102
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +0 -152
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +0 -24
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +0 -24
- package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +0 -69
- package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +0 -71
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +0 -56
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +0 -16
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +0 -39
- package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +0 -153
- package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +0 -55
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +0 -88
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +0 -72
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +0 -54
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.test.mjs +0 -20
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +0 -55
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +0 -268
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.test.mjs +0 -19
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +0 -104
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +0 -661
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +0 -552
- package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +0 -141
- package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +0 -81
- package/src/runtime/agent/orchestrator/tools/spawn-binary-fetcher.test.mjs +0 -52
- package/src/runtime/attachments/store.test.mjs +0 -209
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +0 -17
- package/src/runtime/channels/lib/webhook/deliveries.test.mjs +0 -18
- package/src/runtime/channels/lib/webhook/relay-tunnel.test.mjs +0 -68
- package/src/runtime/media/adapters/codex-image.test.mjs +0 -22
- package/src/runtime/media/download.test.mjs +0 -52
- package/src/runtime/media/renditions.test.mjs +0 -99
- package/src/runtime/media/store.test.mjs +0 -159
- package/src/runtime/memory/lib/archive-security.test.mjs +0 -128
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +0 -33
- package/src/runtime/memory/lib/http-wire.test.mjs +0 -15
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +0 -49
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +0 -78
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +0 -50
- package/src/runtime/memory/lib/pg/process.test.mjs +0 -36
- package/src/runtime/memory/lib/trace-store.test.mjs +0 -57
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +0 -52
- package/src/runtime/shared/background-tasks.test.mjs +0 -78
- package/src/runtime/shared/bounded-download.test.mjs +0 -41
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -25
- package/src/runtime/shared/child-spawn-remote.test.mjs +0 -124
- package/src/runtime/shared/remote-intent.test.mjs +0 -40
- package/src/runtime/shared/resource-admission.test.mjs +0 -43
- package/src/runtime/shared/runtime-root.test.mjs +0 -34
- package/src/runtime/shared/schedule-time.test.mjs +0 -29
- package/src/runtime/shared/session-runtime-health.test.mjs +0 -83
- package/src/runtime/shared/skill-document.test.mjs +0 -81
- package/src/runtime/shared/tool-surface.test.mjs +0 -87
- package/src/runtime/shared/update-checker.test.mjs +0 -33
- package/src/runtime/shared/user-cwd.test.mjs +0 -43
- package/src/runtime/shared/webhook-session-run.test.mjs +0 -14
- package/src/session-runtime/agent-disable.test.mjs +0 -123
- package/src/session-runtime/cwd-plugins.test.mjs +0 -75
- package/src/session-runtime/cwd-tool-routing.test.mjs +0 -61
- package/src/session-runtime/global-extensions.test.mjs +0 -28
- package/src/session-runtime/goal-runtime.test.mjs +0 -108
- package/src/session-runtime/lifecycle-api.test.mjs +0 -21
- package/src/session-runtime/model-settings-persist.test.mjs +0 -105
- package/src/session-runtime/notification-bus.test.mjs +0 -148
- package/src/session-runtime/plugin-mcp-standard.test.mjs +0 -177
- package/src/session-runtime/prewarm.test.mjs +0 -52
- package/src/session-runtime/provider-request-snapshot.test.mjs +0 -37
- package/src/session-runtime/resource-api-global.test.mjs +0 -69
- package/src/session-runtime/session-lifecycle.test.mjs +0 -125
- package/src/session-runtime/session-title.test.mjs +0 -36
- package/src/session-runtime/skills-api.test.mjs +0 -99
- package/src/session-runtime/tool-policy-surface.test.mjs +0 -331
- package/src/standalone/agent-tool/helpers.test.mjs +0 -62
- package/src/standalone/agent-tool/notify.test.mjs +0 -37
- package/src/standalone/agent-tool/spawn-flow.test.mjs +0 -63
- package/src/standalone/channel-restart.test.mjs +0 -145
- package/src/standalone/channel-session-router.test.mjs +0 -37
- package/src/standalone/channel-worker-heartbeat.test.mjs +0 -30
- package/src/standalone/hook-bus/handlers.test.mjs +0 -88
- package/src/standalone/plugin-admin.test.mjs +0 -173
- package/src/standalone/session-runtime-agent-control-client.test.mjs +0 -71
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +0 -139
- package/src/standalone/session-runtime-host-factory.test.mjs +0 -31
- package/src/standalone/session-runtime-host-health.test.mjs +0 -160
- package/src/standalone/session-runtime-inline-host.test.mjs +0 -83
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +0 -80
- package/src/standalone/session-runtime-shard-host.test.mjs +0 -569
- package/src/standalone/session-runtime-shard-router.test.mjs +0 -101
- package/src/tui/app/live-spinner-visibility.test.mjs +0 -44
- package/src/tui/app/panel-epoch.test.mjs +0 -1009
- package/src/tui/app/panel-handoff.test.mjs +0 -311
- package/src/tui/app/prompt-submit.test.mjs +0 -206
- package/src/tui/session/completion-card-restore.test.mjs +0 -111
- package/src/tui/session/goal-continuation.test.mjs +0 -84
- package/src/tui/session/oauth-flows.test.mjs +0 -134
- package/src/tui/session/queue-helpers.test.mjs +0 -46
- package/src/tui/session/session-action-surface.test.mjs +0 -108
- package/src/tui/session/task-wait-submit.test.mjs +0 -80
- package/src/tui/session/tool-result-status.test.mjs +0 -131
- package/src/tui/session/tui-steering-persist.test.mjs +0 -104
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { writeJsonAtomicAsync } from '../runtime/shared/atomic-file.mjs';
|
|
6
|
+
import { compactSessionTitle, SESSION_TITLE_TIMEOUT_MS } from './session-title.mjs';
|
|
6
7
|
|
|
7
|
-
export const DEFAULT_GOAL_TIME_LIMIT_MS =
|
|
8
|
+
export const DEFAULT_GOAL_TIME_LIMIT_MS = 0;
|
|
9
|
+
export const DEFAULT_COMPLETED_GOAL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
10
|
export const MAX_GOAL_TIME_LIMIT_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
11
|
export const GOAL_STATUS_VALUES = Object.freeze([
|
|
10
12
|
'active',
|
|
@@ -18,48 +20,38 @@ export const GOAL_STATUS_VALUES = Object.freeze([
|
|
|
18
20
|
const GOAL_FILE_VERSION = 1;
|
|
19
21
|
const SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
20
22
|
const MAX_OBJECTIVE_LENGTH = 4_000;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
+
const MAX_GOAL_TASKS = 20;
|
|
24
|
+
const MAX_GOAL_TASK_LENGTH = 500;
|
|
25
|
+
const MAX_GOAL_BLOCKER_LENGTH = 1_000;
|
|
23
26
|
const ACTIVE_AGENT_STATUSES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
|
|
24
27
|
|
|
25
|
-
const
|
|
28
|
+
const goalTaskSchema = {
|
|
26
29
|
type: 'object',
|
|
27
30
|
properties: {
|
|
28
|
-
id: { type: 'string', description: 'Stable
|
|
29
|
-
text: { type: 'string', description: '
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
id: { type: 'string', description: 'Stable id from goal status; omit for new work.' },
|
|
32
|
+
text: { type: 'string', description: 'Required work or verification outcome.' },
|
|
33
|
+
status: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['pending', 'in_progress', 'completed'],
|
|
36
|
+
description: 'Mark completed only when fully accomplished.',
|
|
37
|
+
},
|
|
38
|
+
kind: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: ['work', 'verification'],
|
|
41
|
+
description: 'Verification directly checks the finished objective.',
|
|
42
|
+
},
|
|
32
43
|
},
|
|
33
|
-
required: ['text', '
|
|
44
|
+
required: ['text', 'status', 'kind'],
|
|
34
45
|
additionalProperties: false,
|
|
35
46
|
};
|
|
36
47
|
|
|
37
48
|
export const GOAL_TOOL_DEFS = Object.freeze([
|
|
38
49
|
{
|
|
39
|
-
name: '
|
|
40
|
-
title: '
|
|
41
|
-
description: '
|
|
42
|
-
annotations: {
|
|
43
|
-
title: 'Get Goal',
|
|
44
|
-
readOnlyHint: true,
|
|
45
|
-
destructiveHint: false,
|
|
46
|
-
idempotentHint: true,
|
|
47
|
-
openWorldHint: false,
|
|
48
|
-
agentHidden: true,
|
|
49
|
-
},
|
|
50
|
-
inputSchema: {
|
|
51
|
-
type: 'object',
|
|
52
|
-
properties: {},
|
|
53
|
-
required: [],
|
|
54
|
-
additionalProperties: false,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
name: 'create_goal',
|
|
59
|
-
title: 'Create Goal',
|
|
60
|
-
description: 'Create a durable Goal only when the user explicitly asks for Goal mode. Fails while an unfinished Goal exists.',
|
|
50
|
+
name: 'goal',
|
|
51
|
+
title: 'Goal',
|
|
52
|
+
description: 'Manage durable tasks with an idle reminder for unfinished work. Use for 3+ distinct steps, multiple operations/tasks, or careful planning; skip trivial single-step and conversational work. If mutation needs approval, plan without a Goal. After one plan approval, create in the first execution batch alongside the first independent work tool; never spend a model iteration on Goal alone. Finish every approved step without stepwise approval. For an active Goal, paused is the single user-wait state: pause before asking for any user response needed to continue, never for routine errors, retries, or an explicit user addition. After the response, resume in the first execution batch alongside the next independent work tool and continue immediately. Batch set_tasks with the next independent work tool whenever possible. Create with full tasks and verification; complete only with proof. Block only after the same external impasse stops progress for 3 turns, never for user input or a direction choice.',
|
|
61
53
|
annotations: {
|
|
62
|
-
title: '
|
|
54
|
+
title: 'Goal',
|
|
63
55
|
readOnlyHint: false,
|
|
64
56
|
destructiveHint: false,
|
|
65
57
|
idempotentHint: false,
|
|
@@ -69,55 +61,33 @@ export const GOAL_TOOL_DEFS = Object.freeze([
|
|
|
69
61
|
inputSchema: {
|
|
70
62
|
type: 'object',
|
|
71
63
|
properties: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
maxItems: MAX_CRITERIA,
|
|
77
|
-
description: 'Optional concrete, verifiable completion conditions.',
|
|
64
|
+
action: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
enum: ['status', 'create', 'pause', 'resume', 'set_tasks', 'complete', 'block'],
|
|
67
|
+
description: 'status reads; create starts approved execution; pause is the single user-wait or intentional-stop state; resume continues it; set_tasks replaces the snapshot; complete or block finalizes.',
|
|
78
68
|
},
|
|
69
|
+
objective: { type: 'string', description: 'create: requested task outcome.' },
|
|
79
70
|
time_limit_minutes: {
|
|
80
71
|
type: 'number',
|
|
81
72
|
minimum: 1,
|
|
82
73
|
maximum: MAX_GOAL_TIME_LIMIT_MS / 60_000,
|
|
83
|
-
description: '
|
|
74
|
+
description: 'create: optional limit; omit unless requested.',
|
|
84
75
|
},
|
|
85
|
-
|
|
86
|
-
required: ['objective'],
|
|
87
|
-
additionalProperties: false,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'update_goal',
|
|
92
|
-
title: 'Update Goal',
|
|
93
|
-
description: 'Update success criteria or mark the current Goal complete or genuinely blocked. Completion requires verified evidence and every listed criterion satisfied.',
|
|
94
|
-
annotations: {
|
|
95
|
-
title: 'Update Goal',
|
|
96
|
-
readOnlyHint: false,
|
|
97
|
-
destructiveHint: false,
|
|
98
|
-
idempotentHint: false,
|
|
99
|
-
openWorldHint: false,
|
|
100
|
-
agentHidden: true,
|
|
101
|
-
},
|
|
102
|
-
inputSchema: {
|
|
103
|
-
type: 'object',
|
|
104
|
-
properties: {
|
|
105
|
-
status: {
|
|
106
|
-
type: 'string',
|
|
107
|
-
enum: ['complete', 'blocked'],
|
|
108
|
-
description: 'Set complete only when the whole objective is achieved; set blocked only at a genuine impasse.',
|
|
109
|
-
},
|
|
110
|
-
success_criteria: {
|
|
76
|
+
tasks: {
|
|
111
77
|
type: 'array',
|
|
112
|
-
|
|
113
|
-
maxItems:
|
|
114
|
-
|
|
78
|
+
minItems: 1,
|
|
79
|
+
maxItems: MAX_GOAL_TASKS,
|
|
80
|
+
items: goalTaskSchema,
|
|
81
|
+
description: 'create/set_tasks: full durable task snapshot.',
|
|
82
|
+
},
|
|
83
|
+
blocker: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
minLength: 1,
|
|
86
|
+
maxLength: MAX_GOAL_BLOCKER_LENGTH,
|
|
87
|
+
description: 'block: external impasse still preventing progress after 3 consecutive turns.',
|
|
115
88
|
},
|
|
116
|
-
progress_summary: { type: 'string', description: 'Short current progress summary.' },
|
|
117
|
-
completion_evidence: { type: 'string', description: 'Required when status=complete; concise evidence proving the objective.' },
|
|
118
|
-
blocker: { type: 'string', description: 'Required when status=blocked; the external condition preventing progress.' },
|
|
119
89
|
},
|
|
120
|
-
required: [],
|
|
90
|
+
required: ['action'],
|
|
121
91
|
additionalProperties: false,
|
|
122
92
|
},
|
|
123
93
|
},
|
|
@@ -127,6 +97,30 @@ function clean(value) {
|
|
|
127
97
|
return String(value ?? '').trim();
|
|
128
98
|
}
|
|
129
99
|
|
|
100
|
+
const GOAL_ACTION_FIELDS = Object.freeze({
|
|
101
|
+
status: ['action'],
|
|
102
|
+
create: ['action', 'objective', 'time_limit_minutes'],
|
|
103
|
+
pause: ['action'],
|
|
104
|
+
resume: ['action'],
|
|
105
|
+
set_tasks: ['action', 'tasks'],
|
|
106
|
+
complete: ['action'],
|
|
107
|
+
block: ['action', 'blocker'],
|
|
108
|
+
});
|
|
109
|
+
const GOAL_TOOL_FIELDS = new Set(Object.values(GOAL_ACTION_FIELDS).flat());
|
|
110
|
+
|
|
111
|
+
function validateGoalToolCall(value) {
|
|
112
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
113
|
+
throw new Error('goal arguments must be an object');
|
|
114
|
+
}
|
|
115
|
+
const action = clean(value.action).toLowerCase();
|
|
116
|
+
if (!action) throw new Error('goal action is required');
|
|
117
|
+
const allowed = GOAL_ACTION_FIELDS[action];
|
|
118
|
+
if (!allowed) throw new Error(`goal action must be one of: ${Object.keys(GOAL_ACTION_FIELDS).join(', ')}`);
|
|
119
|
+
const extras = Object.keys(value).filter((key) => !GOAL_TOOL_FIELDS.has(key));
|
|
120
|
+
if (extras.length > 0) throw new Error(`goal arguments contain unknown fields: ${extras.join(', ')}`);
|
|
121
|
+
return action;
|
|
122
|
+
}
|
|
123
|
+
|
|
130
124
|
function assertSessionId(value) {
|
|
131
125
|
const sessionId = clean(value);
|
|
132
126
|
if (!SESSION_ID.test(sessionId)) throw new Error(`goal: invalid session id ${JSON.stringify(value)}`);
|
|
@@ -142,15 +136,24 @@ function validateObjective(value) {
|
|
|
142
136
|
return objective;
|
|
143
137
|
}
|
|
144
138
|
|
|
145
|
-
function
|
|
139
|
+
function validateGoalTaskText(value) {
|
|
146
140
|
const text = clean(value);
|
|
147
|
-
if (!text) throw new Error('goal
|
|
148
|
-
if ([...text].length >
|
|
149
|
-
throw new Error(`goal
|
|
141
|
+
if (!text) throw new Error('goal task text is required');
|
|
142
|
+
if ([...text].length > MAX_GOAL_TASK_LENGTH) {
|
|
143
|
+
throw new Error(`goal task exceeds ${MAX_GOAL_TASK_LENGTH} characters`);
|
|
150
144
|
}
|
|
151
145
|
return text;
|
|
152
146
|
}
|
|
153
147
|
|
|
148
|
+
function validateGoalBlocker(value) {
|
|
149
|
+
const blocker = clean(value);
|
|
150
|
+
if (!blocker) throw new Error('goal block: blocker is required');
|
|
151
|
+
if ([...blocker].length > MAX_GOAL_BLOCKER_LENGTH) {
|
|
152
|
+
throw new Error(`goal blocker exceeds ${MAX_GOAL_BLOCKER_LENGTH} characters`);
|
|
153
|
+
}
|
|
154
|
+
return blocker;
|
|
155
|
+
}
|
|
156
|
+
|
|
154
157
|
export function parseGoalDuration(value) {
|
|
155
158
|
if (typeof value === 'number') {
|
|
156
159
|
if (!Number.isFinite(value) || value <= 0) throw new Error('goal duration must be positive');
|
|
@@ -187,54 +190,82 @@ function durationLabel(milliseconds) {
|
|
|
187
190
|
].filter(Boolean).join(' ');
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
function
|
|
191
|
-
if (input == null) return Array.isArray(previous) ? previous.map((
|
|
192
|
-
if (!Array.isArray(input)) throw new Error('
|
|
193
|
-
if (input.length >
|
|
194
|
-
const previousByText = new Map((Array.isArray(previous) ? previous : []).map((
|
|
193
|
+
function normalizeGoalTasks(input, previous = [], { strict = false } = {}) {
|
|
194
|
+
if (input == null) return Array.isArray(previous) ? previous.map((task) => ({ ...task })) : [];
|
|
195
|
+
if (!Array.isArray(input)) throw new Error('goal tasks must be an array');
|
|
196
|
+
if (input.length > MAX_GOAL_TASKS) throw new Error(`goal tasks support at most ${MAX_GOAL_TASKS} entries`);
|
|
197
|
+
const previousByText = new Map((Array.isArray(previous) ? previous : []).map((task) => [clean(task?.text), task]));
|
|
195
198
|
const seen = new Set();
|
|
199
|
+
const seenIds = new Set();
|
|
196
200
|
return input.map((entry, index) => {
|
|
197
|
-
const source = typeof entry === 'string' ? { text: entry,
|
|
198
|
-
if (!source || typeof source !== 'object') throw new Error(`
|
|
199
|
-
const text =
|
|
200
|
-
if (seen.has(text)) throw new Error(`duplicate
|
|
201
|
+
const source = typeof entry === 'string' ? { text: entry, status: 'pending', kind: 'work' } : entry;
|
|
202
|
+
if (!source || typeof source !== 'object') throw new Error(`goal task ${index + 1} is invalid`);
|
|
203
|
+
const text = validateGoalTaskText(source.text);
|
|
204
|
+
if (seen.has(text)) throw new Error(`duplicate goal task: ${text}`);
|
|
201
205
|
seen.add(text);
|
|
202
206
|
const previousEntry = previousByText.get(text);
|
|
203
|
-
const id = clean(source.id) || clean(previousEntry?.id) || `
|
|
207
|
+
const id = clean(source.id) || clean(previousEntry?.id) || `task_${index + 1}`;
|
|
208
|
+
if (seenIds.has(id)) throw new Error(`duplicate goal task id: ${id}`);
|
|
209
|
+
seenIds.add(id);
|
|
210
|
+
const legacySatisfied = source.satisfied === true;
|
|
211
|
+
const rawStatus = clean(source.status).toLowerCase();
|
|
212
|
+
if (strict && !['pending', 'in_progress', 'completed'].includes(rawStatus)) {
|
|
213
|
+
throw new Error(`goal task ${index + 1} has an invalid status`);
|
|
214
|
+
}
|
|
215
|
+
const rawKind = clean(source.kind).toLowerCase();
|
|
216
|
+
if (strict && !['work', 'verification'].includes(rawKind)) {
|
|
217
|
+
throw new Error(`goal task ${index + 1} has an invalid kind`);
|
|
218
|
+
}
|
|
219
|
+
const status = ['pending', 'in_progress', 'completed'].includes(rawStatus)
|
|
220
|
+
? rawStatus
|
|
221
|
+
: legacySatisfied
|
|
222
|
+
? 'completed'
|
|
223
|
+
: 'pending';
|
|
204
224
|
return {
|
|
205
225
|
id,
|
|
206
226
|
text,
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
status,
|
|
228
|
+
kind: rawKind === 'verification' ? 'verification' : 'work',
|
|
209
229
|
};
|
|
210
230
|
});
|
|
211
231
|
}
|
|
212
232
|
|
|
213
|
-
function
|
|
233
|
+
function taskInputRetains(entry, task) {
|
|
234
|
+
if (typeof entry === 'string') return clean(entry) === task.text;
|
|
235
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
236
|
+
const id = clean(entry.id);
|
|
237
|
+
return id ? id === task.id : clean(entry.text) === task.text;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeStoredGoal(value, sessionId, resumedAt = Date.now()) {
|
|
214
241
|
if (!value || typeof value !== 'object') return null;
|
|
215
242
|
const status = GOAL_STATUS_VALUES.includes(value.status) ? value.status : 'active';
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
Math.max(60_000,
|
|
219
|
-
|
|
243
|
+
const storedTimeLimitMs = Number(value.timeLimitMs);
|
|
244
|
+
const timeLimitMs = Number.isFinite(storedTimeLimitMs) && storedTimeLimitMs > 0
|
|
245
|
+
? Math.min(MAX_GOAL_TIME_LIMIT_MS, Math.max(60_000, storedTimeLimitMs))
|
|
246
|
+
: 0;
|
|
220
247
|
const goal = {
|
|
221
248
|
id: clean(value.id) || randomUUID(),
|
|
222
249
|
sessionId,
|
|
223
250
|
objective: validateObjective(value.objective),
|
|
251
|
+
title: compactSessionTitle(value.title || value.objective),
|
|
224
252
|
status,
|
|
225
|
-
|
|
226
|
-
progressSummary: clean(value.progressSummary),
|
|
253
|
+
tasks: normalizeGoalTasks(value.tasks ?? value.criteria ?? []),
|
|
227
254
|
blocker: clean(value.blocker),
|
|
228
|
-
|
|
255
|
+
failureReason: clean(value.failureReason),
|
|
256
|
+
failureCount: Math.max(0, Math.floor(Number(value.failureCount) || 0)),
|
|
229
257
|
timeLimitMs,
|
|
230
258
|
timeUsedMs: Math.max(0, Number(value.timeUsedMs) || 0),
|
|
231
259
|
createdAt: Math.max(0, Number(value.createdAt) || Date.now()),
|
|
232
260
|
updatedAt: Math.max(0, Number(value.updatedAt) || Date.now()),
|
|
233
|
-
|
|
261
|
+
// Active is durable across process boundaries: preserve the open segment so
|
|
262
|
+
// task updates, runtime eviction, and daemon restart never reset Goal time.
|
|
263
|
+
lastStartedAt: status === 'active'
|
|
264
|
+
? Math.max(0, Number(value.lastStartedAt) || Number(resumedAt) || Date.now())
|
|
265
|
+
: null,
|
|
234
266
|
completedAt: Number(value.completedAt) > 0 ? Number(value.completedAt) : null,
|
|
235
267
|
archivedAt: Number(value.archivedAt) > 0 ? Number(value.archivedAt) : null,
|
|
236
268
|
};
|
|
237
|
-
if (goal.status !== 'active') goal.lastStartedAt = null;
|
|
238
269
|
return goal;
|
|
239
270
|
}
|
|
240
271
|
|
|
@@ -247,25 +278,28 @@ function activeElapsedMs(goal, now = Date.now()) {
|
|
|
247
278
|
function publicGoal(goal, now = Date.now()) {
|
|
248
279
|
if (!goal) return null;
|
|
249
280
|
const timeUsedMs = activeElapsedMs(goal, now);
|
|
250
|
-
const
|
|
251
|
-
const
|
|
281
|
+
const hasTimeLimit = Number(goal.timeLimitMs) > 0;
|
|
282
|
+
const remainingMs = hasTimeLimit
|
|
283
|
+
? Math.max(0, Number(goal.timeLimitMs) - timeUsedMs)
|
|
284
|
+
: null;
|
|
285
|
+
const tasks = normalizeGoalTasks(goal.tasks || []);
|
|
252
286
|
return {
|
|
253
287
|
id: goal.id,
|
|
254
288
|
sessionId: goal.sessionId,
|
|
255
289
|
objective: goal.objective,
|
|
290
|
+
title: goal.title || compactSessionTitle(goal.objective),
|
|
256
291
|
status: goal.status,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
progressSummary: goal.progressSummary || '',
|
|
292
|
+
tasks,
|
|
293
|
+
tasksCompleted: tasks.filter((task) => task.status === 'completed').length,
|
|
294
|
+
tasksTotal: tasks.length,
|
|
261
295
|
blocker: goal.blocker || '',
|
|
262
|
-
completionEvidence: goal.completionEvidence || '',
|
|
263
296
|
timeLimitMs: goal.timeLimitMs,
|
|
264
297
|
timeUsedMs,
|
|
265
298
|
remainingMs,
|
|
266
|
-
deadlineAt: goal.status === 'active' && goal.lastStartedAt
|
|
299
|
+
deadlineAt: hasTimeLimit && goal.status === 'active' && goal.lastStartedAt
|
|
267
300
|
? now + remainingMs
|
|
268
301
|
: null,
|
|
302
|
+
snapshotAt: now,
|
|
269
303
|
createdAt: goal.createdAt,
|
|
270
304
|
updatedAt: goal.updatedAt,
|
|
271
305
|
lastStartedAt: goal.lastStartedAt,
|
|
@@ -274,19 +308,66 @@ function publicGoal(goal, now = Date.now()) {
|
|
|
274
308
|
};
|
|
275
309
|
}
|
|
276
310
|
|
|
311
|
+
function normalizedCompletedGoalTtlMs(value) {
|
|
312
|
+
const ttlMs = Number(value);
|
|
313
|
+
return Number.isFinite(ttlMs) && ttlMs >= 0
|
|
314
|
+
? ttlMs
|
|
315
|
+
: DEFAULT_COMPLETED_GOAL_TTL_MS;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function completedGoalExpired(goal, at, ttlMs = DEFAULT_COMPLETED_GOAL_TTL_MS) {
|
|
319
|
+
if (goal?.status !== 'complete') return false;
|
|
320
|
+
const completedAt = Number(goal.completedAt)
|
|
321
|
+
|| Number(goal.updatedAt)
|
|
322
|
+
|| Number(goal.createdAt)
|
|
323
|
+
|| 0;
|
|
324
|
+
return completedAt > 0 && at - completedAt >= normalizedCompletedGoalTtlMs(ttlMs);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function goalFilePath(dataDir, sessionId) {
|
|
328
|
+
return join(clean(dataDir) || process.cwd(), 'goals', `${assertSessionId(sessionId)}.json`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function deleteStoredGoalFile(dataDir, sessionId) {
|
|
332
|
+
try {
|
|
333
|
+
rmSync(goalFilePath(dataDir, sessionId), { force: true });
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
277
340
|
function stopActiveClock(goal, now = Date.now()) {
|
|
278
|
-
if (goal.
|
|
279
|
-
goal.timeUsedMs =
|
|
341
|
+
if (goal.lastStartedAt) {
|
|
342
|
+
goal.timeUsedMs = Math.max(0, Number(goal.timeUsedMs) || 0)
|
|
343
|
+
+ Math.max(0, now - Number(goal.lastStartedAt));
|
|
280
344
|
}
|
|
281
345
|
goal.lastStartedAt = null;
|
|
282
346
|
}
|
|
283
347
|
|
|
284
|
-
function
|
|
348
|
+
function checkpointActiveClock(goal, now = Date.now()) {
|
|
349
|
+
if (!goal.lastStartedAt) return;
|
|
350
|
+
goal.timeUsedMs = Math.max(0, Number(goal.timeUsedMs) || 0)
|
|
351
|
+
+ Math.max(0, now - Number(goal.lastStartedAt));
|
|
352
|
+
goal.lastStartedAt = now;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function clearTurnFailures(goal) {
|
|
356
|
+
goal.failureReason = '';
|
|
357
|
+
goal.failureCount = 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function activateGoal(goal, now = Date.now()) {
|
|
285
361
|
goal.status = 'active';
|
|
286
362
|
goal.lastStartedAt = now;
|
|
287
363
|
goal.completedAt = null;
|
|
288
364
|
goal.archivedAt = null;
|
|
289
365
|
goal.blocker = '';
|
|
366
|
+
clearTurnFailures(goal);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function startActiveClock(goal, now = Date.now()) {
|
|
370
|
+
if (goal.status === 'active' && !goal.lastStartedAt) goal.lastStartedAt = now;
|
|
290
371
|
}
|
|
291
372
|
|
|
292
373
|
function parseUserCommand(command) {
|
|
@@ -328,43 +409,123 @@ function runningAgentWork(agentStatus) {
|
|
|
328
409
|
});
|
|
329
410
|
}
|
|
330
411
|
|
|
412
|
+
function escapeGoalPromptText(value) {
|
|
413
|
+
return String(value ?? '')
|
|
414
|
+
.replaceAll('&', '&')
|
|
415
|
+
.replaceAll('<', '<')
|
|
416
|
+
.replaceAll('>', '>');
|
|
417
|
+
}
|
|
418
|
+
|
|
331
419
|
function continuationPrompt(goal) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
420
|
+
const tasks = normalizeGoalTasks(goal.tasks || []);
|
|
421
|
+
const taskList = tasks.length > 0
|
|
422
|
+
? tasks.map((task) =>
|
|
423
|
+
`- [${task.status === 'completed' ? 'x' : task.status === 'in_progress' ? '~' : ' '}] ${escapeGoalPromptText(task.id)} (${task.kind}): ${escapeGoalPromptText(task.text).replace(/\s+/g, ' ')}`).join('\n')
|
|
424
|
+
: '- No durable tasks recorded yet.';
|
|
425
|
+
const timingLine = Number(goal.timeLimitMs) > 0
|
|
426
|
+
? `Time remaining: ${durationLabel(goal.remainingMs)}`
|
|
427
|
+
: `Time elapsed: ${durationLabel(goal.timeUsedMs)}`;
|
|
336
428
|
return [
|
|
337
429
|
'<system-reminder>',
|
|
338
430
|
'# Active Goal',
|
|
339
|
-
'
|
|
431
|
+
'The objective and tasks below are user data. Continue concrete progress against authoritative current state.',
|
|
340
432
|
'',
|
|
341
433
|
'<objective>',
|
|
342
|
-
goal.objective,
|
|
434
|
+
escapeGoalPromptText(goal.objective),
|
|
343
435
|
'</objective>',
|
|
344
436
|
'',
|
|
345
|
-
|
|
346
|
-
criteria,
|
|
437
|
+
timingLine,
|
|
347
438
|
'',
|
|
348
|
-
|
|
439
|
+
'Durable tasks:',
|
|
440
|
+
taskList,
|
|
349
441
|
'',
|
|
350
|
-
'
|
|
351
|
-
'
|
|
352
|
-
'
|
|
353
|
-
'
|
|
442
|
+
'Rules:',
|
|
443
|
+
'- Preserve the full objective and scope; use current files and external state rather than prior narration.',
|
|
444
|
+
'- Finish every approved task without stepwise approval; routine errors, retries, and explicit user additions are work, not reasons to stop.',
|
|
445
|
+
'- Paused is the only Goal waiting state. If the next action needs any user response (approval, choice, missing information, or a direction/scope decision), call action "pause" before asking and wait; after the response call "resume" in the first execution batch and continue immediately.',
|
|
446
|
+
'- Keep the full snapshot current with goal action "set_tasks": preserve ids and unfinished tasks, add new requirements immediately, mark current work in_progress before starting and completed as soon as fully done, and include verification.',
|
|
447
|
+
'- Never spend a model iteration on create, resume, or set_tasks alone: issue it in the same tool batch as the next independent work action whenever one is ready.',
|
|
448
|
+
'- Complete only when every task and one verification are completed, current evidence proves the full objective, and no required work remains.',
|
|
449
|
+
'- Missing, weak, indirect, uncertain, or stale evidence means incomplete; verification scope must match the claim.',
|
|
450
|
+
'- Block only when the same external impasse prevents meaningful progress for 3 consecutive Goal turns; never for user input, approval, direction choice, difficulty, uncertainty, incomplete work, or optional clarification.',
|
|
451
|
+
'- Never complete or block merely because time is low or the turn is ending.',
|
|
354
452
|
'</system-reminder>',
|
|
355
453
|
].join('\n');
|
|
356
454
|
}
|
|
357
455
|
|
|
456
|
+
function readStoredGoalFile(dataDir, sessionId, at = Date.now()) {
|
|
457
|
+
const id = assertSessionId(sessionId);
|
|
458
|
+
const path = goalFilePath(dataDir, id);
|
|
459
|
+
try {
|
|
460
|
+
if (!existsSync(path)) return null;
|
|
461
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
462
|
+
return normalizeStoredGoal(parsed?.goal, id, at);
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function readStoredGoalSnapshot({
|
|
469
|
+
dataDir,
|
|
470
|
+
sessionId,
|
|
471
|
+
now = () => Date.now(),
|
|
472
|
+
completedGoalTtlMs = DEFAULT_COMPLETED_GOAL_TTL_MS,
|
|
473
|
+
} = {}) {
|
|
474
|
+
const at = Math.max(0, Number(now()) || Date.now());
|
|
475
|
+
const goal = publicGoal(readStoredGoalFile(dataDir, sessionId, at), at);
|
|
476
|
+
if (completedGoalExpired(goal, at, completedGoalTtlMs)) {
|
|
477
|
+
deleteStoredGoalFile(dataDir, sessionId);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
return goal?.archivedAt ? null : goal;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function listStoredActiveGoalSessionIds({
|
|
484
|
+
dataDir,
|
|
485
|
+
now = () => Date.now(),
|
|
486
|
+
completedGoalTtlMs = DEFAULT_COMPLETED_GOAL_TTL_MS,
|
|
487
|
+
} = {}) {
|
|
488
|
+
const root = join(clean(dataDir) || process.cwd(), 'goals');
|
|
489
|
+
let entries = [];
|
|
490
|
+
try {
|
|
491
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
492
|
+
} catch {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const at = Math.max(0, Number(now()) || Date.now());
|
|
496
|
+
const sessionIds = [];
|
|
497
|
+
for (const entry of entries) {
|
|
498
|
+
if (!entry?.isFile?.() || !entry.name.endsWith('.json')) continue;
|
|
499
|
+
const sessionId = entry.name.slice(0, -'.json'.length);
|
|
500
|
+
if (!SESSION_ID.test(sessionId)) continue;
|
|
501
|
+
const goal = publicGoal(readStoredGoalFile(dataDir, sessionId, at), at);
|
|
502
|
+
if (completedGoalExpired(goal, at, completedGoalTtlMs)) {
|
|
503
|
+
deleteStoredGoalFile(dataDir, sessionId);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (goal?.status === 'active' && !goal.archivedAt) sessionIds.push(sessionId);
|
|
507
|
+
}
|
|
508
|
+
return sessionIds.sort();
|
|
509
|
+
}
|
|
510
|
+
|
|
358
511
|
export function createGoalRuntime({
|
|
359
512
|
dataDir,
|
|
360
513
|
now = () => Date.now(),
|
|
361
514
|
defaultTimeLimitMs = DEFAULT_GOAL_TIME_LIMIT_MS,
|
|
515
|
+
completedGoalTtlMs = DEFAULT_COMPLETED_GOAL_TTL_MS,
|
|
516
|
+
generateTitle = null,
|
|
362
517
|
} = {}) {
|
|
363
518
|
const root = join(clean(dataDir) || process.cwd(), 'goals');
|
|
519
|
+
const completedRetentionMs = normalizedCompletedGoalTtlMs(completedGoalTtlMs);
|
|
364
520
|
const cache = new Map();
|
|
365
521
|
const listeners = new Set();
|
|
366
522
|
const deadlineTimers = new Map();
|
|
367
523
|
const writeChains = new Map();
|
|
524
|
+
const mutationChains = new Map();
|
|
525
|
+
const turnGoalIds = new Map();
|
|
526
|
+
const turnStartedAt = new Map();
|
|
527
|
+
const titleJobs = new Map();
|
|
528
|
+
let closed = false;
|
|
368
529
|
|
|
369
530
|
const pathFor = (sessionId) => join(root, `${assertSessionId(sessionId)}.json`);
|
|
370
531
|
|
|
@@ -381,7 +542,7 @@ export function createGoalRuntime({
|
|
|
381
542
|
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
382
543
|
record = {
|
|
383
544
|
version: GOAL_FILE_VERSION,
|
|
384
|
-
goal: normalizeStoredGoal(parsed?.goal, id),
|
|
545
|
+
goal: normalizeStoredGoal(parsed?.goal, id, now()),
|
|
385
546
|
};
|
|
386
547
|
}
|
|
387
548
|
} catch {
|
|
@@ -413,8 +574,18 @@ export function createGoalRuntime({
|
|
|
413
574
|
return write;
|
|
414
575
|
};
|
|
415
576
|
|
|
416
|
-
const
|
|
417
|
-
const
|
|
577
|
+
const withMutation = (sessionId, operation) => {
|
|
578
|
+
const id = assertSessionId(sessionId);
|
|
579
|
+
const previous = mutationChains.get(id) || Promise.resolve();
|
|
580
|
+
const current = previous.catch(() => {}).then(() => operation(id));
|
|
581
|
+
mutationChains.set(id, current);
|
|
582
|
+
current.finally(() => {
|
|
583
|
+
if (mutationChains.get(id) === current) mutationChains.delete(id);
|
|
584
|
+
}).catch(() => {});
|
|
585
|
+
return current;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const emit = (sessionId, goal = visibleSnapshot(sessionId)) => {
|
|
418
589
|
for (const listener of [...listeners]) {
|
|
419
590
|
try { listener({ sessionId, goal }); } catch {}
|
|
420
591
|
}
|
|
@@ -429,19 +600,66 @@ export function createGoalRuntime({
|
|
|
429
600
|
|
|
430
601
|
const commit = async (sessionId, goal) => {
|
|
431
602
|
const id = assertSessionId(sessionId);
|
|
432
|
-
const
|
|
603
|
+
const committedGoal = goal
|
|
604
|
+
? {
|
|
605
|
+
...goal,
|
|
606
|
+
sessionId: id,
|
|
607
|
+
tasks: Array.isArray(goal.tasks)
|
|
608
|
+
? goal.tasks.map((task) => ({ ...task }))
|
|
609
|
+
: [],
|
|
610
|
+
}
|
|
611
|
+
: null;
|
|
612
|
+
const record = { version: GOAL_FILE_VERSION, goal: committedGoal };
|
|
433
613
|
cache.set(id, { record, mtimeMs: cache.get(id)?.mtimeMs || 0 });
|
|
434
614
|
armDeadline(id);
|
|
435
615
|
emit(id);
|
|
436
616
|
await persist(id, record);
|
|
437
|
-
return publicGoal(
|
|
617
|
+
return publicGoal(committedGoal, now());
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const scheduleGoalTitle = (sessionId, goal) => {
|
|
621
|
+
if (closed || typeof generateTitle !== 'function' || !goal) return;
|
|
622
|
+
const id = assertSessionId(sessionId);
|
|
623
|
+
const goalId = clean(goal.id);
|
|
624
|
+
const objective = clean(goal.objective);
|
|
625
|
+
titleJobs.get(id)?.abort.abort(new Error('Goal title generation superseded.'));
|
|
626
|
+
const abort = new AbortController();
|
|
627
|
+
const job = { abort };
|
|
628
|
+
titleJobs.set(id, job);
|
|
629
|
+
let timer = null;
|
|
630
|
+
const timeout = new Promise((_, reject) => {
|
|
631
|
+
timer = setTimeout(() => {
|
|
632
|
+
const error = new Error('Goal title generation timed out.');
|
|
633
|
+
abort.abort(error);
|
|
634
|
+
reject(error);
|
|
635
|
+
}, SESSION_TITLE_TIMEOUT_MS);
|
|
636
|
+
timer.unref?.();
|
|
637
|
+
});
|
|
638
|
+
void Promise.race([
|
|
639
|
+
Promise.resolve().then(() => generateTitle(objective, { signal: abort.signal })),
|
|
640
|
+
timeout,
|
|
641
|
+
]).then((rawTitle) => {
|
|
642
|
+
if (closed || abort.signal.aborted || titleJobs.get(id) !== job) return;
|
|
643
|
+
const title = compactSessionTitle(rawTitle);
|
|
644
|
+
if (!title || title === goal.title) return;
|
|
645
|
+
return withMutation(id, async () => {
|
|
646
|
+
const current = readRecord(id).goal;
|
|
647
|
+
if (!current || current.id !== goalId || current.objective !== objective) return;
|
|
648
|
+
current.title = title;
|
|
649
|
+
current.updatedAt = now();
|
|
650
|
+
await commit(id, current);
|
|
651
|
+
});
|
|
652
|
+
}).catch(() => {}).finally(() => {
|
|
653
|
+
if (timer) clearTimeout(timer);
|
|
654
|
+
if (titleJobs.get(id) === job) titleJobs.delete(id);
|
|
655
|
+
});
|
|
438
656
|
};
|
|
439
657
|
|
|
440
658
|
const limitIfExpired = (sessionId) => {
|
|
441
659
|
const id = assertSessionId(sessionId);
|
|
442
660
|
const record = readRecord(id);
|
|
443
661
|
const goal = record.goal;
|
|
444
|
-
if (!goal || goal.status !== 'active') return false;
|
|
662
|
+
if (!goal || goal.status !== 'active' || !(Number(goal.timeLimitMs) > 0)) return false;
|
|
445
663
|
const at = now();
|
|
446
664
|
if (activeElapsedMs(goal, at) < goal.timeLimitMs) return false;
|
|
447
665
|
stopActiveClock(goal, at);
|
|
@@ -455,7 +673,7 @@ export function createGoalRuntime({
|
|
|
455
673
|
function armDeadline(sessionId) {
|
|
456
674
|
clearDeadline(sessionId);
|
|
457
675
|
const goal = readRecord(sessionId).goal;
|
|
458
|
-
if (!goal || goal.status !== 'active') return;
|
|
676
|
+
if (!goal || goal.status !== 'active' || !goal.lastStartedAt || !(Number(goal.timeLimitMs) > 0)) return;
|
|
459
677
|
const remainingMs = Math.max(0, goal.timeLimitMs - activeElapsedMs(goal, now()));
|
|
460
678
|
if (remainingMs <= 0) {
|
|
461
679
|
queueMicrotask(() => limitIfExpired(sessionId));
|
|
@@ -463,7 +681,7 @@ export function createGoalRuntime({
|
|
|
463
681
|
}
|
|
464
682
|
const timer = setTimeout(() => {
|
|
465
683
|
deadlineTimers.delete(sessionId);
|
|
466
|
-
limitIfExpired(sessionId);
|
|
684
|
+
void withMutation(sessionId, () => limitIfExpired(sessionId)).catch(() => {});
|
|
467
685
|
}, remainingMs);
|
|
468
686
|
timer.unref?.();
|
|
469
687
|
deadlineTimers.set(sessionId, timer);
|
|
@@ -472,7 +690,16 @@ export function createGoalRuntime({
|
|
|
472
690
|
const storedSnapshot = (sessionId) => {
|
|
473
691
|
const id = assertSessionId(sessionId);
|
|
474
692
|
limitIfExpired(id);
|
|
475
|
-
|
|
693
|
+
const at = now();
|
|
694
|
+
const goal = publicGoal(readRecord(id).goal, at);
|
|
695
|
+
if (!completedGoalExpired(goal, at, completedRetentionMs)) return goal;
|
|
696
|
+
clearDeadline(id);
|
|
697
|
+
cache.set(id, {
|
|
698
|
+
record: { version: GOAL_FILE_VERSION, goal: null },
|
|
699
|
+
mtimeMs: 0,
|
|
700
|
+
});
|
|
701
|
+
deleteStoredGoalFile(dataDir, id);
|
|
702
|
+
return null;
|
|
476
703
|
};
|
|
477
704
|
|
|
478
705
|
function visibleSnapshot(sessionId) {
|
|
@@ -493,20 +720,27 @@ export function createGoalRuntime({
|
|
|
493
720
|
throw new Error('cannot create a new Goal while an unfinished Goal exists');
|
|
494
721
|
}
|
|
495
722
|
const at = now();
|
|
723
|
+
const configuredDefaultTimeLimitMs = Number(defaultTimeLimitMs);
|
|
496
724
|
const timeLimitMs = args.timeLimitMs != null
|
|
497
725
|
? parseGoalDuration(args.timeLimitMs)
|
|
498
726
|
: args.duration
|
|
499
727
|
? parseGoalDuration(args.duration)
|
|
500
|
-
:
|
|
728
|
+
: Number.isFinite(configuredDefaultTimeLimitMs) && configuredDefaultTimeLimitMs > 0
|
|
729
|
+
? parseGoalDuration(configuredDefaultTimeLimitMs)
|
|
730
|
+
: 0;
|
|
731
|
+
const initialTasks = Array.isArray(args.tasks)
|
|
732
|
+
? normalizeGoalTasks(args.tasks.filter((task) => clean(task?.text)), [], { strict: true })
|
|
733
|
+
: [];
|
|
501
734
|
const goal = {
|
|
502
735
|
id: randomUUID(),
|
|
503
736
|
sessionId: id,
|
|
504
737
|
objective: validateObjective(args.objective),
|
|
738
|
+
title: compactSessionTitle(args.objective),
|
|
505
739
|
status: 'active',
|
|
506
|
-
|
|
507
|
-
progressSummary: '',
|
|
740
|
+
tasks: initialTasks,
|
|
508
741
|
blocker: '',
|
|
509
|
-
|
|
742
|
+
failureReason: '',
|
|
743
|
+
failureCount: 0,
|
|
510
744
|
timeLimitMs,
|
|
511
745
|
timeUsedMs: 0,
|
|
512
746
|
createdAt: at,
|
|
@@ -515,53 +749,92 @@ export function createGoalRuntime({
|
|
|
515
749
|
completedAt: null,
|
|
516
750
|
archivedAt: null,
|
|
517
751
|
};
|
|
518
|
-
|
|
752
|
+
if (args.startInCurrentTurn === true) {
|
|
753
|
+
const startedAt = turnStartedAt.get(id) || at;
|
|
754
|
+
turnGoalIds.set(id, goal.id);
|
|
755
|
+
turnStartedAt.set(id, startedAt);
|
|
756
|
+
goal.lastStartedAt = startedAt;
|
|
757
|
+
}
|
|
758
|
+
const created = await commit(id, goal);
|
|
759
|
+
scheduleGoalTitle(id, goal);
|
|
760
|
+
return created;
|
|
519
761
|
};
|
|
520
762
|
|
|
521
|
-
const updateGoal = async (sessionId, args = {}, { user = false } = {}) => {
|
|
763
|
+
const updateGoal = async (sessionId, args = {}, { user = false, expectedGoalId = '' } = {}) => {
|
|
522
764
|
const id = assertSessionId(sessionId);
|
|
523
765
|
const goal = requireGoal(id);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
goal.criteria = normalizeCriteria(args.successCriteria ?? args.success_criteria, goal.criteria);
|
|
527
|
-
}
|
|
528
|
-
if (args.progressSummary != null || args.progress_summary != null) {
|
|
529
|
-
goal.progressSummary = clean(args.progressSummary ?? args.progress_summary);
|
|
766
|
+
if (clean(expectedGoalId) && clean(expectedGoalId) !== clean(goal.id)) {
|
|
767
|
+
throw new Error('stale Goal update rejected because the active Goal changed');
|
|
530
768
|
}
|
|
769
|
+
const at = now();
|
|
531
770
|
const status = clean(args.status).toLowerCase();
|
|
532
|
-
if (status
|
|
533
|
-
|
|
771
|
+
if (!status) throw new Error('update_goal status is required');
|
|
772
|
+
if (!['active', 'complete', 'blocked'].includes(status)) {
|
|
773
|
+
throw new Error('update_goal can only set status active, complete, or blocked');
|
|
534
774
|
}
|
|
535
|
-
if (status === '
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const missing = goal.criteria.filter((criterion) => !criterion.satisfied);
|
|
539
|
-
if (!user && missing.length > 0) {
|
|
540
|
-
throw new Error(`cannot complete Goal: ${missing.length} success criteria remain unsatisfied`);
|
|
775
|
+
if (status === 'active') {
|
|
776
|
+
if (goal.status === 'complete') {
|
|
777
|
+
throw new Error('a completed Goal cannot be resumed; edit it or create a new Goal');
|
|
541
778
|
}
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
evidence: criterion.evidence || 'Confirmed by user',
|
|
547
|
-
}));
|
|
779
|
+
if (goal.status !== 'active') activateGoal(goal, at);
|
|
780
|
+
else {
|
|
781
|
+
goal.blocker = '';
|
|
782
|
+
clearTurnFailures(goal);
|
|
548
783
|
}
|
|
549
|
-
|
|
784
|
+
} else if (status === 'complete') {
|
|
785
|
+
const tasks = normalizeGoalTasks(goal.tasks || []);
|
|
786
|
+
if (tasks.length === 0) {
|
|
787
|
+
throw new Error('cannot complete Goal: create at least one durable task first');
|
|
788
|
+
}
|
|
789
|
+
const incomplete = tasks.filter((task) => task.status !== 'completed');
|
|
790
|
+
if (incomplete.length > 0) {
|
|
791
|
+
throw new Error(`cannot complete Goal: ${incomplete.length} durable tasks remain incomplete`);
|
|
792
|
+
}
|
|
793
|
+
if (!tasks.some((task) => task.kind === 'verification' && task.status === 'completed')) {
|
|
794
|
+
throw new Error('cannot complete Goal: complete at least one verification task first');
|
|
795
|
+
}
|
|
796
|
+
if (turnGoalIds.get(id) === goal.id) checkpointActiveClock(goal, at);
|
|
797
|
+
else stopActiveClock(goal, at);
|
|
550
798
|
goal.status = 'complete';
|
|
551
|
-
goal.completionEvidence = evidence || 'Completed by user';
|
|
552
799
|
goal.completedAt = at;
|
|
553
800
|
goal.blocker = '';
|
|
801
|
+
clearTurnFailures(goal);
|
|
554
802
|
} else if (status === 'blocked') {
|
|
555
|
-
const blocker =
|
|
556
|
-
if (
|
|
557
|
-
stopActiveClock(goal, at);
|
|
803
|
+
const blocker = validateGoalBlocker(args.blocker);
|
|
804
|
+
if (turnGoalIds.get(id) === goal.id) checkpointActiveClock(goal, at);
|
|
805
|
+
else stopActiveClock(goal, at);
|
|
558
806
|
goal.status = 'blocked';
|
|
559
807
|
goal.blocker = blocker;
|
|
808
|
+
clearTurnFailures(goal);
|
|
560
809
|
}
|
|
561
810
|
goal.updatedAt = at;
|
|
562
811
|
return commit(id, goal);
|
|
563
812
|
};
|
|
564
813
|
|
|
814
|
+
const setGoalTasks = async (sessionId, args = {}, { expectedGoalId = '' } = {}) => {
|
|
815
|
+
const id = assertSessionId(sessionId);
|
|
816
|
+
const goal = requireGoal(id);
|
|
817
|
+
if (clean(expectedGoalId) && clean(expectedGoalId) !== clean(goal.id)) {
|
|
818
|
+
throw new Error('stale Goal task update rejected because the active Goal changed');
|
|
819
|
+
}
|
|
820
|
+
if (goal.status === 'complete') {
|
|
821
|
+
throw new Error('cannot update tasks for a completed Goal');
|
|
822
|
+
}
|
|
823
|
+
if (!Array.isArray(args.tasks) || args.tasks.length === 0) {
|
|
824
|
+
throw new Error('goal set_tasks requires at least one task');
|
|
825
|
+
}
|
|
826
|
+
const previousTasks = normalizeGoalTasks(goal.tasks || []);
|
|
827
|
+
const omitted = previousTasks.filter((task) =>
|
|
828
|
+
task.status !== 'completed' && !args.tasks.some((entry) => taskInputRetains(entry, task)));
|
|
829
|
+
if (omitted.length > 0) {
|
|
830
|
+
const detail = omitted.map((task) => `${task.id} (${task.text})`).join(', ');
|
|
831
|
+
throw new Error(`cannot remove unfinished Goal tasks: ${detail}`);
|
|
832
|
+
}
|
|
833
|
+
goal.tasks = normalizeGoalTasks(args.tasks, previousTasks, { strict: true });
|
|
834
|
+
goal.updatedAt = now();
|
|
835
|
+
return commit(id, goal);
|
|
836
|
+
};
|
|
837
|
+
|
|
565
838
|
const control = async (sessionId, rawArgs = {}) => {
|
|
566
839
|
const id = assertSessionId(sessionId);
|
|
567
840
|
const args = typeof rawArgs === 'string'
|
|
@@ -574,11 +847,17 @@ export function createGoalRuntime({
|
|
|
574
847
|
if (action === 'create') {
|
|
575
848
|
goal = await createGoal(id, {
|
|
576
849
|
objective: args.objective,
|
|
577
|
-
successCriteria: args.successCriteria ?? args.success_criteria,
|
|
578
850
|
duration: args.duration,
|
|
579
851
|
timeLimitMs: args.timeLimitMs,
|
|
580
852
|
});
|
|
581
|
-
return {
|
|
853
|
+
return {
|
|
854
|
+
ok: true,
|
|
855
|
+
action,
|
|
856
|
+
goal,
|
|
857
|
+
message: Number(goal.timeLimitMs) > 0
|
|
858
|
+
? `Goal active · ${goal.objective} · ${durationLabel(goal.remainingMs)} remaining`
|
|
859
|
+
: `Goal active · ${goal.objective} · ${durationLabel(goal.timeUsedMs)} elapsed`,
|
|
860
|
+
};
|
|
582
861
|
}
|
|
583
862
|
if (action === 'get' || action === 'status') {
|
|
584
863
|
goal = visibleSnapshot(id);
|
|
@@ -587,7 +866,11 @@ export function createGoalRuntime({
|
|
|
587
866
|
action: 'get',
|
|
588
867
|
goal,
|
|
589
868
|
message: goal
|
|
590
|
-
? `Goal ${goal.status} · ${goal.objective}${goal.status === 'active'
|
|
869
|
+
? `Goal ${goal.status} · ${goal.objective}${goal.status === 'active'
|
|
870
|
+
? Number(goal.timeLimitMs) > 0
|
|
871
|
+
? ` · ${durationLabel(goal.remainingMs)} remaining`
|
|
872
|
+
: ` · ${durationLabel(goal.timeUsedMs)} elapsed`
|
|
873
|
+
: ''}`
|
|
591
874
|
: 'No visible Goal for this session',
|
|
592
875
|
};
|
|
593
876
|
}
|
|
@@ -596,34 +879,49 @@ export function createGoalRuntime({
|
|
|
596
879
|
return { ok: true, action, goal: null, message: 'Goal cleared' };
|
|
597
880
|
}
|
|
598
881
|
goal = requireGoal(id);
|
|
882
|
+
const expectedGoalId = clean(args.expectedGoalId);
|
|
883
|
+
if (expectedGoalId && expectedGoalId !== clean(goal.id)) {
|
|
884
|
+
throw new Error('stale Goal update rejected because the active Goal changed');
|
|
885
|
+
}
|
|
599
886
|
const at = now();
|
|
600
887
|
if (action === 'pause') {
|
|
601
888
|
if (goal.status === 'active') stopActiveClock(goal, at);
|
|
602
889
|
goal.status = 'paused';
|
|
890
|
+
goal.blocker = '';
|
|
891
|
+
clearTurnFailures(goal);
|
|
603
892
|
goal.updatedAt = at;
|
|
604
893
|
goal = await commit(id, goal);
|
|
605
894
|
return { ok: true, action, goal, message: `Goal paused · ${goal.objective}` };
|
|
606
895
|
}
|
|
607
896
|
if (action === 'resume') {
|
|
897
|
+
if (goal.status === 'complete') {
|
|
898
|
+
throw new Error('a completed Goal cannot be resumed; edit it or create a new Goal');
|
|
899
|
+
}
|
|
608
900
|
const added = args.duration ? parseGoalDuration(args.duration) : null;
|
|
609
901
|
stopActiveClock(goal, at);
|
|
610
902
|
if (added != null) goal.timeLimitMs = Math.min(MAX_GOAL_TIME_LIMIT_MS, goal.timeUsedMs + added);
|
|
611
|
-
if (goal.timeLimitMs <= goal.timeUsedMs)
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
startActiveClock(goal, at);
|
|
903
|
+
if (goal.timeLimitMs > 0 && goal.timeLimitMs <= goal.timeUsedMs) goal.timeLimitMs = 0;
|
|
904
|
+
activateGoal(goal, at);
|
|
615
905
|
goal.updatedAt = at;
|
|
616
906
|
goal = await commit(id, goal);
|
|
617
|
-
return {
|
|
907
|
+
return {
|
|
908
|
+
ok: true,
|
|
909
|
+
action,
|
|
910
|
+
goal,
|
|
911
|
+
message: Number(goal.timeLimitMs) > 0
|
|
912
|
+
? `Goal resumed · ${goal.objective} · ${durationLabel(goal.remainingMs)} remaining`
|
|
913
|
+
: `Goal resumed · ${goal.objective} · ${durationLabel(goal.timeUsedMs)} elapsed`,
|
|
914
|
+
};
|
|
618
915
|
}
|
|
619
916
|
if (action === 'edit') {
|
|
620
917
|
goal.objective = validateObjective(args.objective);
|
|
621
|
-
goal.
|
|
622
|
-
goal.
|
|
623
|
-
goal.
|
|
624
|
-
|
|
918
|
+
goal.title = compactSessionTitle(goal.objective);
|
|
919
|
+
goal.tasks = [];
|
|
920
|
+
if (goal.status === 'complete') activateGoal(goal, at);
|
|
921
|
+
else clearTurnFailures(goal);
|
|
625
922
|
goal.updatedAt = at;
|
|
626
923
|
goal = await commit(id, goal);
|
|
924
|
+
scheduleGoalTitle(id, goal);
|
|
627
925
|
return { ok: true, action, goal, message: `Goal updated · ${goal.objective}` };
|
|
628
926
|
}
|
|
629
927
|
if (action === 'time') {
|
|
@@ -643,11 +941,8 @@ export function createGoalRuntime({
|
|
|
643
941
|
return { ok: true, action, goal, message: `Goal time limit · ${durationLabel(limit)}` };
|
|
644
942
|
}
|
|
645
943
|
if (action === 'complete') {
|
|
646
|
-
goal = await updateGoal(id, {
|
|
647
|
-
|
|
648
|
-
completionEvidence: clean(args.completionEvidence) || 'Completed by user',
|
|
649
|
-
}, { user: true });
|
|
650
|
-
return { ok: true, action, goal, message: `Goal complete · ${goal.objective} · ${durationLabel(goal.timeUsedMs)} in progress` };
|
|
944
|
+
goal = await updateGoal(id, { status: 'complete' }, { user: true });
|
|
945
|
+
return { ok: true, action, goal, message: `Goal complete · ${goal.objective} · ${durationLabel(goal.timeUsedMs)} elapsed` };
|
|
651
946
|
}
|
|
652
947
|
throw new Error(`unknown Goal action: ${action}`);
|
|
653
948
|
};
|
|
@@ -655,22 +950,66 @@ export function createGoalRuntime({
|
|
|
655
950
|
const executeTool = async (name, args = {}, context = {}) => {
|
|
656
951
|
const sessionId = context.callerSessionId || context.sessionId;
|
|
657
952
|
const id = assertSessionId(sessionId);
|
|
953
|
+
if (name === 'goal') {
|
|
954
|
+
const action = validateGoalToolCall(args);
|
|
955
|
+
if (action === 'status') {
|
|
956
|
+
const goal = storedSnapshot(id);
|
|
957
|
+
return JSON.stringify({ goal, remaining_ms: goal?.remainingMs ?? null });
|
|
958
|
+
}
|
|
959
|
+
if (action === 'create') {
|
|
960
|
+
let timeLimitMs;
|
|
961
|
+
if (Object.hasOwn(args, 'time_limit_minutes')) {
|
|
962
|
+
const minutes = Number(args.time_limit_minutes);
|
|
963
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
964
|
+
throw new Error('goal time_limit_minutes must be a positive number');
|
|
965
|
+
}
|
|
966
|
+
timeLimitMs = minutes * 60_000;
|
|
967
|
+
}
|
|
968
|
+
const goal = await withMutation(id, () => createGoal(id, {
|
|
969
|
+
objective: args.objective,
|
|
970
|
+
tasks: args.tasks,
|
|
971
|
+
startInCurrentTurn: true,
|
|
972
|
+
...(timeLimitMs != null ? { timeLimitMs } : {}),
|
|
973
|
+
}));
|
|
974
|
+
return JSON.stringify({ goal, remaining_ms: goal.remainingMs });
|
|
975
|
+
}
|
|
976
|
+
const expectedGoalId = turnGoalIds.get(id) || storedSnapshot(id)?.id || '';
|
|
977
|
+
if (action === 'pause' || action === 'resume') {
|
|
978
|
+
const result = await withMutation(id, () => control(id, { action, expectedGoalId }));
|
|
979
|
+
return JSON.stringify({ goal: result.goal, remaining_ms: result.goal.remainingMs });
|
|
980
|
+
}
|
|
981
|
+
if (action === 'set_tasks') {
|
|
982
|
+
const goal = await withMutation(id, () => setGoalTasks(id, args, { expectedGoalId }));
|
|
983
|
+
return JSON.stringify({ goal, remaining_ms: goal.remainingMs });
|
|
984
|
+
}
|
|
985
|
+
const goal = await withMutation(id, () => updateGoal(id, {
|
|
986
|
+
status: action === 'block' ? 'blocked' : 'complete',
|
|
987
|
+
...(action === 'block' ? { blocker: args.blocker } : {}),
|
|
988
|
+
}, { expectedGoalId }));
|
|
989
|
+
return JSON.stringify({ goal, remaining_ms: goal.remainingMs });
|
|
990
|
+
}
|
|
991
|
+
// Runtime-only compatibility for in-flight calls from pre-unification sessions.
|
|
658
992
|
if (name === 'get_goal') {
|
|
659
993
|
const goal = storedSnapshot(id);
|
|
660
|
-
return JSON.stringify({ goal, remaining_ms: goal?.remainingMs ?? null }
|
|
994
|
+
return JSON.stringify({ goal, remaining_ms: goal?.remainingMs ?? null });
|
|
661
995
|
}
|
|
662
996
|
if (name === 'create_goal') {
|
|
663
997
|
const minutes = Number(args.time_limit_minutes);
|
|
664
|
-
const goal = await createGoal(id, {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
});
|
|
669
|
-
return JSON.stringify({ goal, remaining_ms: goal.remainingMs }, null, 2);
|
|
998
|
+
const goal = await withMutation(id, () => createGoal(id, {
|
|
999
|
+
objective: args.objective,
|
|
1000
|
+
...(Number.isFinite(minutes) && minutes > 0 ? { timeLimitMs: minutes * 60_000 } : {}),
|
|
1001
|
+
}));
|
|
1002
|
+
return JSON.stringify({ goal, remaining_ms: goal.remainingMs });
|
|
670
1003
|
}
|
|
671
1004
|
if (name === 'update_goal') {
|
|
672
|
-
const
|
|
673
|
-
|
|
1005
|
+
const expectedGoalId = turnGoalIds.get(id) || storedSnapshot(id)?.id || '';
|
|
1006
|
+
const goal = await withMutation(id, () => updateGoal(id, args, { expectedGoalId }));
|
|
1007
|
+
return JSON.stringify({ goal, remaining_ms: goal.remainingMs });
|
|
1008
|
+
}
|
|
1009
|
+
if (name === 'set_goal_tasks') {
|
|
1010
|
+
const expectedGoalId = turnGoalIds.get(id) || storedSnapshot(id)?.id || '';
|
|
1011
|
+
const goal = await withMutation(id, () => setGoalTasks(id, args, { expectedGoalId }));
|
|
1012
|
+
return JSON.stringify({ goal, remaining_ms: goal.remainingMs });
|
|
674
1013
|
}
|
|
675
1014
|
throw new Error(`unknown Goal tool: ${name}`);
|
|
676
1015
|
};
|
|
@@ -687,6 +1026,7 @@ export function createGoalRuntime({
|
|
|
687
1026
|
if (!sessionId) return null;
|
|
688
1027
|
const goal = visibleSnapshot(sessionId);
|
|
689
1028
|
armDeadline(sessionId);
|
|
1029
|
+
emit(sessionId, goal);
|
|
690
1030
|
return goal;
|
|
691
1031
|
},
|
|
692
1032
|
subscribe(listener) {
|
|
@@ -694,8 +1034,80 @@ export function createGoalRuntime({
|
|
|
694
1034
|
listeners.add(listener);
|
|
695
1035
|
return () => listeners.delete(listener);
|
|
696
1036
|
},
|
|
697
|
-
control,
|
|
1037
|
+
control(sessionId, args = {}) {
|
|
1038
|
+
return withMutation(sessionId, () => control(sessionId, args));
|
|
1039
|
+
},
|
|
698
1040
|
executeTool,
|
|
1041
|
+
startTurn(sessionId) {
|
|
1042
|
+
return withMutation(sessionId, async (id) => {
|
|
1043
|
+
const goal = readRecord(id).goal;
|
|
1044
|
+
if (!goal) {
|
|
1045
|
+
turnGoalIds.delete(id);
|
|
1046
|
+
turnStartedAt.delete(id);
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
const at = now();
|
|
1050
|
+
turnGoalIds.set(id, goal.id);
|
|
1051
|
+
turnStartedAt.set(id, at);
|
|
1052
|
+
if (goal.status !== 'active') return visibleSnapshot(id);
|
|
1053
|
+
startActiveClock(goal, at);
|
|
1054
|
+
goal.updatedAt = at;
|
|
1055
|
+
return commit(id, goal);
|
|
1056
|
+
});
|
|
1057
|
+
},
|
|
1058
|
+
settleTurn(sessionId, detail = {}) {
|
|
1059
|
+
return withMutation(sessionId, async (id) => {
|
|
1060
|
+
const expectedGoalId = turnGoalIds.get(id) || '';
|
|
1061
|
+
turnGoalIds.delete(id);
|
|
1062
|
+
turnStartedAt.delete(id);
|
|
1063
|
+
const goal = readRecord(id).goal;
|
|
1064
|
+
if (!goal || (expectedGoalId && goal.id !== expectedGoalId)) return visibleSnapshot(id);
|
|
1065
|
+
const at = now();
|
|
1066
|
+
if (detail?.preserveGoalState === true) {
|
|
1067
|
+
if (goal.status === 'active') checkpointActiveClock(goal, at);
|
|
1068
|
+
goal.updatedAt = at;
|
|
1069
|
+
return commit(id, goal);
|
|
1070
|
+
}
|
|
1071
|
+
const status = clean(typeof detail === 'string' ? detail : detail.status).toLowerCase();
|
|
1072
|
+
const usageLimited = detail?.usageLimited === true || detail?.usage_limited === true;
|
|
1073
|
+
if (usageLimited && ['active', 'budget_limited'].includes(goal.status)) {
|
|
1074
|
+
stopActiveClock(goal, at);
|
|
1075
|
+
clearTurnFailures(goal);
|
|
1076
|
+
goal.status = 'usage_limited';
|
|
1077
|
+
goal.blocker = clean(detail?.error) || 'Provider usage limit reached';
|
|
1078
|
+
} else if (status === 'cancelled' && goal.status === 'active') {
|
|
1079
|
+
stopActiveClock(goal, at);
|
|
1080
|
+
clearTurnFailures(goal);
|
|
1081
|
+
goal.status = 'paused';
|
|
1082
|
+
goal.blocker = '';
|
|
1083
|
+
} else if (status === 'failed' && goal.status === 'active') {
|
|
1084
|
+
const failureReason = clean(detail?.error) || 'Goal turn failed';
|
|
1085
|
+
if (goal.failureReason === failureReason) goal.failureCount += 1;
|
|
1086
|
+
else {
|
|
1087
|
+
goal.failureReason = failureReason;
|
|
1088
|
+
goal.failureCount = 1;
|
|
1089
|
+
}
|
|
1090
|
+
goal.blocker = '';
|
|
1091
|
+
if (goal.failureCount >= 3) {
|
|
1092
|
+
stopActiveClock(goal, at);
|
|
1093
|
+
goal.status = 'blocked';
|
|
1094
|
+
goal.blocker = failureReason;
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
clearTurnFailures(goal);
|
|
1098
|
+
}
|
|
1099
|
+
if (goal.status === 'active') {
|
|
1100
|
+
checkpointActiveClock(goal, at);
|
|
1101
|
+
if (goal.timeLimitMs > 0 && goal.timeUsedMs >= goal.timeLimitMs) {
|
|
1102
|
+
stopActiveClock(goal, at);
|
|
1103
|
+
goal.status = 'budget_limited';
|
|
1104
|
+
goal.timeUsedMs = Math.max(goal.timeUsedMs, goal.timeLimitMs);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
goal.updatedAt = at;
|
|
1108
|
+
return commit(id, goal);
|
|
1109
|
+
});
|
|
1110
|
+
},
|
|
699
1111
|
continuation(sessionId, { agentStatus = null } = {}) {
|
|
700
1112
|
const goal = visibleSnapshot(sessionId);
|
|
701
1113
|
if (!goal || goal.status !== 'active') return { run: false, reason: goal?.status || 'missing', goal };
|
|
@@ -704,17 +1116,25 @@ export function createGoalRuntime({
|
|
|
704
1116
|
},
|
|
705
1117
|
async archiveCompletedOnUserInput(sessionId) {
|
|
706
1118
|
if (!sessionId) return null;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1119
|
+
return withMutation(sessionId, async (id) => {
|
|
1120
|
+
const goal = readRecord(id).goal;
|
|
1121
|
+
if (!goal || goal.status !== 'complete' || goal.archivedAt) return visibleSnapshot(id);
|
|
1122
|
+
const at = now();
|
|
1123
|
+
goal.archivedAt = at;
|
|
1124
|
+
goal.updatedAt = at;
|
|
1125
|
+
await commit(id, goal);
|
|
1126
|
+
return null;
|
|
1127
|
+
});
|
|
714
1128
|
},
|
|
715
1129
|
close() {
|
|
1130
|
+
closed = true;
|
|
1131
|
+
for (const job of titleJobs.values()) job.abort.abort(new Error('Goal runtime closed.'));
|
|
1132
|
+
titleJobs.clear();
|
|
716
1133
|
for (const timer of deadlineTimers.values()) clearTimeout(timer);
|
|
717
1134
|
deadlineTimers.clear();
|
|
1135
|
+
mutationChains.clear();
|
|
1136
|
+
turnGoalIds.clear();
|
|
1137
|
+
turnStartedAt.clear();
|
|
718
1138
|
listeners.clear();
|
|
719
1139
|
},
|
|
720
1140
|
};
|