mixdog 1.0.12 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -181
- package/package.json +4 -2
- package/scripts/build-shipped-history.mjs +131 -0
- package/scripts/lib/run-node-tests.mjs +235 -13
- package/scripts/lib/test-failure-records.mjs +5 -0
- package/scripts/lib/test-failure-summary.mjs +9 -3
- package/scripts/lib/test-spawn-ledger.mjs +19 -0
- package/scripts/prune-embedding-runtime.mjs +9 -5
- package/scripts/test-direct.mjs +16 -1
- package/src/agents/advisor/AGENT.md +38 -0
- package/src/agents/advisor/agent.json +6 -0
- package/src/agents/reviewer/AGENT.md +5 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +381 -0
- package/src/defaults/skills/goal-management/SKILL.md +11 -5
- package/src/defaults/skills/setup/SKILL.md +5 -8
- package/src/defaults/skills/setup/references/actions.md +61 -31
- package/src/defaults/skills/setup/references/surfaces.md +12 -10
- package/src/lib/rules-builder.cjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
- package/src/runtime/agent/orchestrator/config.mjs +102 -21
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
- package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
- package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
- package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
- package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
- package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
- package/src/runtime/channels/lib/config.mjs +3 -8
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
- package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
- package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
- package/src/runtime/computer-bridge/actions.mjs +2 -0
- package/src/runtime/effort-judge/judge-client.mjs +12 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
- package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
- package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
- package/src/runtime/memory/lib/memory-embed.mjs +5 -0
- package/src/runtime/shared/agent-route-config.mjs +50 -2
- package/src/runtime/shared/bounded-file-read.mjs +166 -0
- package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
- package/src/runtime/shared/config.mjs +24 -4
- package/src/runtime/shared/defaults-separation.mjs +263 -0
- package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
- package/src/runtime/shared/llm/model-catalog.mjs +23 -0
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
- package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
- package/src/runtime/shared/orchestration.mjs +14 -6
- package/src/runtime/shared/projects.mjs +65 -48
- package/src/runtime/shared/provider-accounts.mjs +47 -6
- package/src/runtime/shared/quota-resume-store.mjs +255 -0
- package/src/runtime/shared/shipped-definitions.mjs +275 -0
- package/src/runtime/shared/tool-card-model.golden.json +3 -3
- package/src/runtime/shared/tool-result-summary.mjs +86 -15
- package/src/runtime/shared/transcript-metadata.mjs +2 -0
- package/src/runtime/shared/turn-review-reply.mjs +2 -0
- package/src/runtime/shared/turn-snapshot.mjs +65 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
- package/src/session-runtime/boot/apis.mjs +19 -0
- package/src/session-runtime/boot/config.mjs +4 -0
- package/src/session-runtime/boot/facade.mjs +4 -1
- package/src/session-runtime/boot/infrastructure.mjs +21 -2
- package/src/session-runtime/boot/session.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
- package/src/session-runtime/config-lifecycle.mjs +31 -6
- package/src/session-runtime/goal-facade-api.mjs +4 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
- package/src/session-runtime/media-api.mjs +32 -22
- package/src/session-runtime/model-recency.mjs +14 -0
- package/src/session-runtime/model-route/route-tuning.mjs +3 -5
- package/src/session-runtime/model-route/set-route.mjs +49 -21
- package/src/session-runtime/model-route-api.mjs +2 -1
- package/src/session-runtime/notification-bus.mjs +31 -6
- package/src/session-runtime/provider-auth/login.mjs +6 -0
- package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
- package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
- package/src/session-runtime/resource-api.mjs +8 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +20 -1
- package/src/session-runtime/runtime-review-api.mjs +12 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +87 -13
- package/src/session-runtime/services/provider-admin.mjs +256 -63
- package/src/session-runtime/services/seeds.mjs +73 -142
- package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
- package/src/session-runtime/services/usage-dashboard.mjs +7 -0
- package/src/session-runtime/session-address.mjs +125 -0
- package/src/session-runtime/session-create.mjs +57 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
- package/src/session-runtime/settings-compaction-api.mjs +13 -0
- package/src/session-runtime/setup-tool/executor.mjs +92 -40
- package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
- package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
- package/src/session-runtime/tool-surface.mjs +16 -9
- package/src/session-runtime/turn/session-ops.mjs +3 -0
- package/src/session-runtime/turn/share-ops.mjs +4 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
- package/src/session-runtime/usage-stats-api.mjs +15 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
- package/src/standalone/daemon-boot-coordinator.mjs +10 -0
- package/src/standalone/daemon-drain-policy.mjs +9 -0
- package/src/standalone/daemon-stored-session-views.mjs +7 -8
- package/src/standalone/daemon.mjs +21 -5
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client.mjs +52 -7
- package/src/standalone/session-protocol.mjs +8 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
- package/src/standalone/session-service/entries/session-loads.mjs +9 -0
- package/src/standalone/session-service/projection/revisions.mjs +28 -4
- package/src/standalone/session-service/projection.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +140 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
- package/src/standalone/session-service/session-calls.mjs +10 -2
- package/src/standalone/session-service/stored-reader.mjs +26 -1
- package/src/standalone/session-service.mjs +21 -0
- package/src/standalone/session-transport/routes.mjs +2 -0
- package/src/tui/app/doctor.mjs +7 -11
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
- package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
- package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
- package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
- package/src/tui/components/ContextPanel.jsx +5 -1
- package/src/tui/components/Spinner.jsx +34 -319
- package/src/tui/components/SpinnerByline.jsx +256 -0
- package/src/tui/components/use-spinner-progress.mjs +113 -0
- package/src/tui/dist/index.mjs +15540 -14452
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
- package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
- package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
- package/src/tui/session/agent-job-feed.mjs +4 -0
- package/src/tui/session/context-state.mjs +13 -1
- package/src/tui/session/initial-state.mjs +6 -0
- package/src/tui/session/quota-resume.mjs +263 -0
- package/src/tui/session/session-api/intake/abort.mjs +39 -35
- package/src/tui/session/session-api/intake/submission.mjs +13 -21
- package/src/tui/session/session-api/intake.mjs +3 -3
- package/src/tui/session/session-api/integrations.mjs +5 -1
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
- package/src/tui/session/session-api/lifecycle.mjs +71 -2
- package/src/tui/session/session-api/media.mjs +2 -2
- package/src/tui/session/session-api/quota-resume.mjs +21 -0
- package/src/tui/session/session-api/routes.mjs +2 -0
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +14 -6
- package/src/tui/session/session-flow/drain.mjs +52 -5
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
- package/src/tui/session/session-flow/queue.mjs +3 -0
- package/src/tui/session/session-flow/steering.mjs +11 -0
- package/src/tui/session/session-flow.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +7 -2
- package/src/tui/session/transcript-intake.mjs +1 -0
- package/src/tui/session/turn.mjs +40 -3
- package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
|
@@ -22,7 +22,15 @@ function ackModelVisible(event) {
|
|
|
22
22
|
|
|
23
23
|
// The `execution-ui` delivery: one transcript card per execution card key,
|
|
24
24
|
// then the model-visible completion body enqueued once per execution.
|
|
25
|
-
export function createExecutionDelivery({
|
|
25
|
+
export function createExecutionDelivery({
|
|
26
|
+
dedup,
|
|
27
|
+
pendingResume,
|
|
28
|
+
enqueue,
|
|
29
|
+
nextId,
|
|
30
|
+
pushResponse,
|
|
31
|
+
statusRefresh,
|
|
32
|
+
getAdmission = () => 'open',
|
|
33
|
+
}) {
|
|
26
34
|
function pushCard({ event, text, parsed, delivery, executionId, status, terminal }) {
|
|
27
35
|
const cardKey = executionCardKey(event, text, parsed);
|
|
28
36
|
const firstDelivery = !cardKey || !dedup.hasNotificationKey(cardKey);
|
|
@@ -65,6 +73,10 @@ export function createExecutionDelivery({ dedup, pendingResume, enqueue, nextId,
|
|
|
65
73
|
function enqueueCompletion({ event, text, delivery, executionId, notificationKey }) {
|
|
66
74
|
const resumeBody = String(delivery.modelContent || '').trim();
|
|
67
75
|
if (!resumeBody) return;
|
|
76
|
+
// Validated at consumption (not only at the bus): a closing/closed owner
|
|
77
|
+
// keeps the card for UI/history but never queues a model-visible twin, and
|
|
78
|
+
// does not ack it as model-delivered.
|
|
79
|
+
if (getAdmission() !== 'open' || event?.meta?.owner_closed === true) return;
|
|
68
80
|
const completionKey = executionResumeKey(resumeBody, executionId);
|
|
69
81
|
if (pendingResume.isDiscarded(completionKey) || isDeliveredCompletion({ executionId, text: resumeBody })) {
|
|
70
82
|
ackModelVisible(event);
|
|
@@ -13,6 +13,7 @@ export function createNotificationRouter({
|
|
|
13
13
|
statusRefresh,
|
|
14
14
|
executionDelivery,
|
|
15
15
|
modelNotification,
|
|
16
|
+
getAdmission = () => 'open',
|
|
16
17
|
}) {
|
|
17
18
|
let uiOpenSeq = 0;
|
|
18
19
|
let setupChangeSeq = 0;
|
|
@@ -68,6 +69,8 @@ export function createNotificationRouter({
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
statusRefresh.refresh(parsed);
|
|
72
|
+
// A closing/closed owner enqueues no model work from a notification.
|
|
73
|
+
if (getAdmission() !== 'open' || event?.meta?.owner_closed === true) return false;
|
|
71
74
|
return modelNotification.enqueueNotification({ event, text, delivery, notificationKey });
|
|
72
75
|
};
|
|
73
76
|
}
|
|
@@ -25,6 +25,7 @@ export function createPendingResume({
|
|
|
25
25
|
now,
|
|
26
26
|
tombstoneTtlMs,
|
|
27
27
|
tombstoneLimit,
|
|
28
|
+
getAdmission = () => 'open',
|
|
28
29
|
}) {
|
|
29
30
|
let deferred = false;
|
|
30
31
|
// Completion keys explicitly abandoned by Esc. Tombstones are per-feed
|
|
@@ -88,8 +89,20 @@ export function createPendingResume({
|
|
|
88
89
|
function kick(body = '', completionKey = '') {
|
|
89
90
|
const key = executionResumeKey(body, completionKey);
|
|
90
91
|
if (body && isDiscarded(key)) return;
|
|
92
|
+
// Admission fence: a closed session never resumes from a completion; a
|
|
93
|
+
// closing one defers (a failed close flushes the deferred kick).
|
|
94
|
+
const admission = getAdmission();
|
|
95
|
+
if (admission === 'terminal') {
|
|
96
|
+
bodies.length = 0;
|
|
97
|
+
deferred = false;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
91
100
|
if (body) bodies.push({ body, key });
|
|
92
101
|
if (getDisposed()) return;
|
|
102
|
+
if (admission === 'closing') {
|
|
103
|
+
deferred = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
93
106
|
if (getState().busy) {
|
|
94
107
|
deferred = true;
|
|
95
108
|
return;
|
|
@@ -104,7 +117,7 @@ export function createPendingResume({
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
function flushDeferred() {
|
|
107
|
-
if (!deferred || getDisposed() || getState().busy) return;
|
|
120
|
+
if (!deferred || getDisposed() || getState().busy || getAdmission() !== 'open') return;
|
|
108
121
|
kick();
|
|
109
122
|
}
|
|
110
123
|
|
|
@@ -40,6 +40,7 @@ export function createAgentJobFeed({
|
|
|
40
40
|
now = () => Date.now(),
|
|
41
41
|
executionResumeTombstoneTtlMs = 30_000,
|
|
42
42
|
executionResumeTombstoneLimit = 128,
|
|
43
|
+
getAdmission = () => 'open',
|
|
43
44
|
}) {
|
|
44
45
|
const dedup = createExecutionDedup({ displayedNotificationKeys: displayedExecutionNotificationKeys });
|
|
45
46
|
const pendingResume = createPendingResume({
|
|
@@ -51,6 +52,7 @@ export function createAgentJobFeed({
|
|
|
51
52
|
now,
|
|
52
53
|
tombstoneTtlMs: executionResumeTombstoneTtlMs,
|
|
53
54
|
tombstoneLimit: executionResumeTombstoneLimit,
|
|
55
|
+
getAdmission,
|
|
54
56
|
});
|
|
55
57
|
const card = createAgentJobCard({ getState, itemIndexById, patchItem });
|
|
56
58
|
const statusRefresh = createAgentStatusRefresh({ set, getDisposed, agentStatusState });
|
|
@@ -61,6 +63,7 @@ export function createAgentJobFeed({
|
|
|
61
63
|
nextId,
|
|
62
64
|
pushResponse: pushAsyncAgentResponse || pushUserOrSyntheticItem,
|
|
63
65
|
statusRefresh,
|
|
66
|
+
getAdmission,
|
|
64
67
|
});
|
|
65
68
|
const modelNotification = createModelNotificationEnqueue({
|
|
66
69
|
chain: createNotificationEnqueueChain({ pushNotice }),
|
|
@@ -76,6 +79,7 @@ export function createAgentJobFeed({
|
|
|
76
79
|
statusRefresh,
|
|
77
80
|
executionDelivery,
|
|
78
81
|
modelNotification,
|
|
82
|
+
getAdmission,
|
|
79
83
|
});
|
|
80
84
|
|
|
81
85
|
function subscribeRuntimeNotifications() {
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
import { contextMeasurementStats } from '../../runtime/shared/context-measurement.mjs';
|
|
9
9
|
import { goalStateSnapshot } from '../../runtime/agent/orchestrator/runtime-core/goal-state.mjs';
|
|
10
10
|
|
|
11
|
-
export function createContextState({
|
|
11
|
+
export function createContextState({
|
|
12
|
+
runtime,
|
|
13
|
+
getState,
|
|
14
|
+
updateState,
|
|
15
|
+
getPendingSessionReset,
|
|
16
|
+
getVisibleGoal,
|
|
17
|
+
getQuotaResume = null,
|
|
18
|
+
}) {
|
|
12
19
|
const autoClearState = () =>
|
|
13
20
|
runtime.getAutoClear?.() ||
|
|
14
21
|
runtime.autoClear || {
|
|
@@ -46,6 +53,8 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
46
53
|
clientHostPid: runtime.clientHostPid || null,
|
|
47
54
|
model: runtime.model,
|
|
48
55
|
provider: runtime.provider,
|
|
56
|
+
sessionProvider: runtime.sessionProvider ?? null,
|
|
57
|
+
sessionModel: runtime.sessionModel ?? null,
|
|
49
58
|
effort: runtime.effort,
|
|
50
59
|
effortOptions: runtime.effortOptions,
|
|
51
60
|
fast: runtime.fast,
|
|
@@ -69,6 +78,9 @@ export function createContextState({ runtime, getState, updateState, getPendingS
|
|
|
69
78
|
// 안 보이던 골이 생성되었다 바로 사라짐).
|
|
70
79
|
// Clock-anchored, so an unchanged running Goal reads equal on every pulse.
|
|
71
80
|
goal: goalStateSnapshot(typeof getVisibleGoal === 'function' ? getVisibleGoal() : runtime.goalStatus?.() || null),
|
|
81
|
+
// The daemon-queued quota resume of this session id (identity-stable while
|
|
82
|
+
// unchanged), so a resume or session switch republishes it with the route.
|
|
83
|
+
quotaResume: typeof getQuotaResume === 'function' ? getQuotaResume() : null,
|
|
72
84
|
});
|
|
73
85
|
|
|
74
86
|
const routeState = () => {
|
|
@@ -23,6 +23,12 @@ export function createInitialSessionState({ runtime, runtimeCwd, baseRouteState
|
|
|
23
23
|
queued: [],
|
|
24
24
|
thinking: null,
|
|
25
25
|
toolApproval: null,
|
|
26
|
+
// How the last approval ended and from which device, so every device can
|
|
27
|
+
// say who answered ({ id, approved, device, at }).
|
|
28
|
+
toolApprovalResult: null,
|
|
29
|
+
// A prompt reclaimed by a DIFFERENT device than the one that sent it:
|
|
30
|
+
// { id, ids, text, device, at } — only the sender's composer applies it.
|
|
31
|
+
promptRestore: null,
|
|
26
32
|
lastTurn: null,
|
|
27
33
|
stats: createSessionStats(),
|
|
28
34
|
// Incremental derivations published by the session runtime so App does not scan all
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quota-resume.mjs — one Lead session's side of the quota auto-resume: it
|
|
3
|
+
* queues a resume when a turn stops on exhausted provider quota, cancels it on
|
|
4
|
+
* the inputs that supersede it, owns the one "retry failed turn" operation,
|
|
5
|
+
* and runs the resume when the daemon scheduler fires it.
|
|
6
|
+
*
|
|
7
|
+
* The durable queue itself lives in the daemon-wide store
|
|
8
|
+
* (runtime/shared/quota-resume-store.mjs); this lane never keeps a timer, so an
|
|
9
|
+
* evicted session waits exactly like a live one.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
cancelQuotaResumeEntry,
|
|
13
|
+
forgetQuotaSuspendedGoal,
|
|
14
|
+
noteQuotaSuspendedGoal,
|
|
15
|
+
publicQuotaResume,
|
|
16
|
+
quotaAutoResumeEnabled,
|
|
17
|
+
quotaResumeEntry,
|
|
18
|
+
quotaSuspendedGoal,
|
|
19
|
+
scheduleQuotaResumeEntry,
|
|
20
|
+
subscribeQuotaResume,
|
|
21
|
+
} from '../../runtime/shared/quota-resume-store.mjs';
|
|
22
|
+
import { createSubmissionIntake } from './session-api/intake/submission.mjs';
|
|
23
|
+
|
|
24
|
+
export const RETRY_CONTINUE_INSTRUCTION = 'Continue from where you left off.';
|
|
25
|
+
|
|
26
|
+
// Rows that frame a turn rather than carry its output (desktop transcript-rows).
|
|
27
|
+
const TURN_CHROME_KINDS = new Set(['user', 'turndone', 'statusdone', 'notice']);
|
|
28
|
+
|
|
29
|
+
function stableItemKey(item, index) {
|
|
30
|
+
if (item?.id !== undefined && item?.id !== null) return String(item.id);
|
|
31
|
+
return `${item?.kind || 'item'}:${item?.text || item?.label || item?.status || ''}:${index}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Turn identity per transcript row — the same keys the desktop derives. */
|
|
35
|
+
export function transcriptTurnKeys(items) {
|
|
36
|
+
const transcript = Array.isArray(items) ? items : [];
|
|
37
|
+
const keys = [];
|
|
38
|
+
let activeTurn = null;
|
|
39
|
+
for (let index = 0; index < transcript.length; index += 1) {
|
|
40
|
+
const item = transcript[index];
|
|
41
|
+
if (item?.kind === 'user') activeTurn = `turn:${stableItemKey(item, index)}`;
|
|
42
|
+
if (!activeTurn) activeTurn = `segment:${stableItemKey(item, index)}`;
|
|
43
|
+
keys.push(activeTurn);
|
|
44
|
+
if (item?.kind === 'turndone') activeTurn = null;
|
|
45
|
+
}
|
|
46
|
+
return keys;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The failed turndone that ended the transcript on exhausted provider quota,
|
|
50
|
+
* when no later turn has begun: { turnKey, quota: { provider, model, resetAt|null } }. */
|
|
51
|
+
export function latestQuotaFailure(items) {
|
|
52
|
+
const transcript = Array.isArray(items) ? items : [];
|
|
53
|
+
for (let index = transcript.length - 1; index >= 0; index -= 1) {
|
|
54
|
+
const item = transcript[index];
|
|
55
|
+
if (item?.kind === 'user') return null;
|
|
56
|
+
if (item?.kind !== 'turndone') continue;
|
|
57
|
+
const quota = item.quota;
|
|
58
|
+
if (String(item.status || '').toLowerCase() !== 'failed' || !quota || typeof quota.provider !== 'string') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const turnKey = transcriptTurnKeys(transcript)[index] || '';
|
|
62
|
+
if (!turnKey) return null;
|
|
63
|
+
return {
|
|
64
|
+
turnKey,
|
|
65
|
+
quota: {
|
|
66
|
+
provider: quota.provider,
|
|
67
|
+
model: typeof quota.model === 'string' ? quota.model : '',
|
|
68
|
+
resetAt: typeof quota.resetAt === 'number' && Number.isFinite(quota.resetAt) ? quota.resetAt : null,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What retrying the turn submits: its prompt when it produced no output, the
|
|
76
|
+
* continuation instruction when it did, null when the prompt carried no text
|
|
77
|
+
* (a media/attachment-only prompt cannot be replayed). */
|
|
78
|
+
export function failedTurnRetryPrompt(items, turnKey) {
|
|
79
|
+
const transcript = Array.isArray(items) ? items : [];
|
|
80
|
+
const keys = transcriptTurnKeys(transcript);
|
|
81
|
+
let prompt = '';
|
|
82
|
+
let sampled = false;
|
|
83
|
+
let hasAttachments = false;
|
|
84
|
+
for (let index = 0; index < transcript.length; index += 1) {
|
|
85
|
+
if (keys[index] !== turnKey) continue;
|
|
86
|
+
const item = transcript[index];
|
|
87
|
+
if (item?.kind === 'user' && !prompt) {
|
|
88
|
+
prompt = String(item.text ?? '').trim();
|
|
89
|
+
hasAttachments = Array.isArray(item.images) && item.images.length > 0;
|
|
90
|
+
}
|
|
91
|
+
if (!TURN_CHROME_KINDS.has(String(item?.kind || ''))) sampled = true;
|
|
92
|
+
}
|
|
93
|
+
if (!prompt) return null;
|
|
94
|
+
// Transcript rows keep only attachment metadata, so a text-only replay
|
|
95
|
+
// would drop the attachments and the rewind would delete the original row.
|
|
96
|
+
if (hasAttachments && !sampled) return null;
|
|
97
|
+
return sampled ? RETRY_CONTINUE_INSTRUCTION : prompt;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createQuotaResumeLane(bag) {
|
|
101
|
+
const { runtime, flags, getState, set, pending } = bag;
|
|
102
|
+
const currentId = () => String(getState().sessionId || '');
|
|
103
|
+
let intake = null;
|
|
104
|
+
const submitAsync = (...args) => {
|
|
105
|
+
intake ??= createSubmissionIntake(bag);
|
|
106
|
+
return intake.submitAsync(...args);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const publishQuotaResume = () => {
|
|
110
|
+
const next = publicQuotaResume(quotaResumeEntry(currentId()));
|
|
111
|
+
if ((getState().quotaResume ?? null) !== next) set({ quotaResume: next });
|
|
112
|
+
};
|
|
113
|
+
const unsubscribe = subscribeQuotaResume((sessionId) => {
|
|
114
|
+
if (flags.disposed) return;
|
|
115
|
+
if (sessionId === null || sessionId === currentId()) publishQuotaResume();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const requireAddressed = (sessionId) => {
|
|
119
|
+
if (!sessionId || currentId() !== sessionId) throw new Error('session is not the active session');
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// The session's own route (the selected route only before it has one).
|
|
123
|
+
const sessionRoute = () => ({
|
|
124
|
+
routeProvider: String(runtime.sessionProvider ?? runtime.provider ?? ''),
|
|
125
|
+
routeModel: String(runtime.sessionModel ?? runtime.model ?? ''),
|
|
126
|
+
});
|
|
127
|
+
const queueResume = (sessionId, failure) =>
|
|
128
|
+
scheduleQuotaResumeEntry({ sessionId, turnKey: failure.turnKey, ...failure.quota, ...sessionRoute() });
|
|
129
|
+
|
|
130
|
+
// A failed turn is settling between its failed turndone (where the resume is
|
|
131
|
+
// queued) and its Goal settlement (where the suspended Goal is recorded).
|
|
132
|
+
// Every superseding cancel bumps the epoch, so a cancel that lands inside
|
|
133
|
+
// that window also keeps the settlement from recording the Goal.
|
|
134
|
+
let settlement = null;
|
|
135
|
+
let cancelEpoch = 0;
|
|
136
|
+
|
|
137
|
+
const cancelFor = (sessionId) => {
|
|
138
|
+
cancelEpoch += 1;
|
|
139
|
+
cancelQuotaResumeEntry(sessionId);
|
|
140
|
+
forgetQuotaSuspendedGoal(sessionId);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The one retry operation. `automatic` is the scheduler's resume: it
|
|
144
|
+
* enqueues directly, because the hours it waited must not trigger the idle
|
|
145
|
+
* auto-clear a user submission starts. */
|
|
146
|
+
function retryFailedTurn(turnKey, { automatic = false } = {}) {
|
|
147
|
+
const prompt = failedTurnRetryPrompt(getState().items, turnKey);
|
|
148
|
+
if (!prompt) return false;
|
|
149
|
+
if (automatic) {
|
|
150
|
+
return bag.enqueue(prompt, { mode: 'prompt', id: bag.nextId(), displayText: prompt, retryFailedTurn: true });
|
|
151
|
+
}
|
|
152
|
+
return submitAsync(prompt, { retryFailedTurn: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
publishQuotaResume,
|
|
157
|
+
disposeQuotaResume: unsubscribe,
|
|
158
|
+
|
|
159
|
+
/** Turn close, synchronously with the failed turndone: with a known reset
|
|
160
|
+
* and the setting on, queues the resume, so the same publication that
|
|
161
|
+
* shows the failure carries `quotaResume`. */
|
|
162
|
+
beginQuotaSettlement() {
|
|
163
|
+
const id = currentId();
|
|
164
|
+
const failure = latestQuotaFailure(getState().items);
|
|
165
|
+
settlement = null;
|
|
166
|
+
if (!id || !failure) return null;
|
|
167
|
+
settlement = { sessionId: id, turnKey: failure.turnKey, epoch: cancelEpoch };
|
|
168
|
+
if (failure.quota.resetAt === null || !quotaAutoResumeEnabled()) return null;
|
|
169
|
+
return queueResume(id, failure);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/** After the Goal settled: records the Goal this failure suspended, unless
|
|
173
|
+
* a superseding cancel landed since the turn closed. */
|
|
174
|
+
finishQuotaSettlement({ suspendedGoalId = '' } = {}) {
|
|
175
|
+
const current = settlement;
|
|
176
|
+
settlement = null;
|
|
177
|
+
if (!current || current.epoch !== cancelEpoch || !suspendedGoalId) return false;
|
|
178
|
+
return noteQuotaSuspendedGoal(current.sessionId, current.turnKey, suspendedGoalId);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/** New user input supersedes the stopped turn. */
|
|
182
|
+
cancelQuotaResumeOnUserInput() {
|
|
183
|
+
const id = currentId();
|
|
184
|
+
if (id) cancelFor(id);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/** Explicit request to auto-resume the latest quota-failed turn. */
|
|
188
|
+
scheduleQuotaResume(sessionId) {
|
|
189
|
+
requireAddressed(sessionId);
|
|
190
|
+
const failure = latestQuotaFailure(getState().items);
|
|
191
|
+
if (!failure || failure.quota.resetAt === null) {
|
|
192
|
+
throw new Error('latest turn did not fail on exhausted provider quota with a known reset time');
|
|
193
|
+
}
|
|
194
|
+
queueResume(sessionId, failure);
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
cancelQuotaResume(sessionId) {
|
|
199
|
+
requireAddressed(sessionId);
|
|
200
|
+
cancelQuotaResumeEntry(sessionId);
|
|
201
|
+
return true;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/** A route change or a session clear/delete supersedes the wait. */
|
|
205
|
+
supersedeQuotaResume(sessionId) {
|
|
206
|
+
if (sessionId) cancelFor(String(sessionId));
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async retryFailedTurn(sessionId, turnKey) {
|
|
210
|
+
requireAddressed(sessionId);
|
|
211
|
+
return (await retryFailedTurn(String(turnKey || ''))) === true;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/** Scheduler fire for the entry of `generation`, after a usable reading.
|
|
215
|
+
* Synchronous from the generation check to the admitted retry, so no
|
|
216
|
+
* cancellation can land between them. Returns the outcome. */
|
|
217
|
+
fireQuotaResume({ generation } = {}) {
|
|
218
|
+
const id = currentId();
|
|
219
|
+
const entry = quotaResumeEntry(id);
|
|
220
|
+
if (!id || !entry || entry.generation !== generation) return 'stale';
|
|
221
|
+
const state = getState();
|
|
222
|
+
if (
|
|
223
|
+
flags.disposed ||
|
|
224
|
+
settlement !== null ||
|
|
225
|
+
(flags.admission || 'open') !== 'open' ||
|
|
226
|
+
state.busy ||
|
|
227
|
+
state.commandBusy ||
|
|
228
|
+
flags.autoClearRunning ||
|
|
229
|
+
flags.draining ||
|
|
230
|
+
pending.length > 0
|
|
231
|
+
) {
|
|
232
|
+
return 'busy';
|
|
233
|
+
}
|
|
234
|
+
const failure = latestQuotaFailure(state.items);
|
|
235
|
+
if (!failure || failure.turnKey !== entry.turnKey) {
|
|
236
|
+
cancelQuotaResumeEntry(id, { generation });
|
|
237
|
+
return 'superseded';
|
|
238
|
+
}
|
|
239
|
+
const route = sessionRoute();
|
|
240
|
+
if (route.routeProvider !== entry.routeProvider || route.routeModel !== entry.routeModel) {
|
|
241
|
+
cancelQuotaResumeEntry(id, { generation });
|
|
242
|
+
return 'route-changed';
|
|
243
|
+
}
|
|
244
|
+
const goalId = quotaSuspendedGoal(id, entry.turnKey);
|
|
245
|
+
// A user turn replays its prompt; an automatic Goal turn has no user row
|
|
246
|
+
// and resumes as its Goal's next continuation.
|
|
247
|
+
const userTurn = entry.turnKey.startsWith('turn:');
|
|
248
|
+
if (userTurn ? !failedTurnRetryPrompt(state.items, entry.turnKey) : !goalId) {
|
|
249
|
+
cancelQuotaResumeEntry(id, { generation });
|
|
250
|
+
return 'unreplayable';
|
|
251
|
+
}
|
|
252
|
+
if (!cancelQuotaResumeEntry(id, { generation })) return 'stale';
|
|
253
|
+
forgetQuotaSuspendedGoal(id);
|
|
254
|
+
// Queued on the Goal's mutation chain ahead of the retry turn's own
|
|
255
|
+
// start, so that turn opens on the revived Goal; the retry is busy
|
|
256
|
+
// before the revived Goal's continuation check runs, so it queues none.
|
|
257
|
+
// Without a retry the revived Goal's continuation is the one turn.
|
|
258
|
+
if (goalId) void Promise.resolve(runtime.reactivateUsageLimitedGoal?.(goalId)).catch(() => {});
|
|
259
|
+
if (!userTurn) return 'fired';
|
|
260
|
+
return retryFailedTurn(entry.turnKey, { automatic: true }) !== false ? 'fired' : 'rejected';
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Abort: Esc while idle hands a queued
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// requeue entries.
|
|
1
|
+
// Abort: Esc while idle hands a queued submission back to the draft; Esc
|
|
2
|
+
// during a turn cancels it and, unless a steering prompt is already queued,
|
|
3
|
+
// reclaims the in-flight prompt, its history slot and its requeue entries.
|
|
5
4
|
import { hydratePastedAttachments, hydrateRestorableFileParts } from '../../../../runtime/attachments/store.mjs';
|
|
6
5
|
import { abortGoalTurn } from '../../goal-turn-state.mjs';
|
|
7
6
|
import { promptHistoryWithout } from '../../prompt-history.mjs';
|
|
@@ -15,16 +14,15 @@ function hydrateTolerantly(pastedImages, pastedTexts, content = null) {
|
|
|
15
14
|
};
|
|
16
15
|
const hydrated = hydratePastedAttachments(pastedImages, pastedTexts, { onUnreadable });
|
|
17
16
|
const files = hydrateRestorableFileParts(content, { onUnreadable });
|
|
17
|
+
const verb = unreadable === 1 ? ' was' : 's were';
|
|
18
18
|
return {
|
|
19
19
|
...hydrated,
|
|
20
20
|
...(files.length ? { content: files } : {}),
|
|
21
|
-
notice: unreadable
|
|
22
|
-
? `${unreadable} attachment${unreadable === 1 ? ' was' : 's were'} no longer available and dropped.`
|
|
23
|
-
: '',
|
|
21
|
+
notice: unreadable ? `${unreadable} attachment${verb} no longer available and dropped.` : '',
|
|
24
22
|
};
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
export function createAbortAction(bag
|
|
25
|
+
export function createAbortAction(bag) {
|
|
28
26
|
const {
|
|
29
27
|
runtime,
|
|
30
28
|
flags,
|
|
@@ -39,31 +37,12 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
|
|
|
39
37
|
discardExecutionPendingResume,
|
|
40
38
|
} = bag;
|
|
41
39
|
|
|
42
|
-
// Esc while idle: hand a queued
|
|
43
|
-
//
|
|
40
|
+
// Esc while idle: hand a queued submission back to the draft instead of
|
|
41
|
+
// interrupting anything.
|
|
44
42
|
const reclaimIdleSubmission = (submissionId) => {
|
|
45
43
|
if (!submissionId) return false;
|
|
46
44
|
const restored = restoreQueued('', submissionId);
|
|
47
|
-
if (!restored || Number(restored.count) < 1)
|
|
48
|
-
const intake = acceptingSubmissions.get(submissionId);
|
|
49
|
-
if (!intake) return false;
|
|
50
|
-
// Hydrate before cancelling: nothing is lost if an attachment is gone.
|
|
51
|
-
const attachments = hydrateTolerantly(
|
|
52
|
-
intake.queueOptions.pastedImages,
|
|
53
|
-
intake.queueOptions.pastedTexts,
|
|
54
|
-
intake.text
|
|
55
|
-
);
|
|
56
|
-
intake.cancelled = true;
|
|
57
|
-
return {
|
|
58
|
-
aborted: false,
|
|
59
|
-
restoreText: String(intake.queueOptions.displayText || '').trim(),
|
|
60
|
-
pastedImages: attachments.pastedImages,
|
|
61
|
-
pastedTexts: attachments.pastedTexts,
|
|
62
|
-
...(attachments.content ? { content: attachments.content } : {}),
|
|
63
|
-
...(attachments.notice ? { notice: attachments.notice } : {}),
|
|
64
|
-
restoredSubmissionIds: [submissionId],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
45
|
+
if (!restored || Number(restored.count) < 1) return false;
|
|
67
46
|
return {
|
|
68
47
|
aborted: false,
|
|
69
48
|
restoreText: restored.text,
|
|
@@ -77,10 +56,21 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
|
|
|
77
56
|
|
|
78
57
|
// Pull the interrupted prompt back out of the transcript and history and
|
|
79
58
|
// put its unsent requeue entries at the front of the queue.
|
|
80
|
-
const reclaimInFlightPrompt = (restoreState, { restoreText, requeueEntries }) => {
|
|
59
|
+
const reclaimInFlightPrompt = (restoreState, { restoreText, requeueEntries, handoff = false }) => {
|
|
81
60
|
restoreState.reclaimed = true;
|
|
82
61
|
const idSet = new Set((restoreState.submittedIds || []).filter((id) => id != null));
|
|
83
62
|
const patch = { spinner: null, thinking: null, lastTurn: null };
|
|
63
|
+
// Another device cancelled this turn: the prompt travels to the device that
|
|
64
|
+
// sent it through the state, never through the canceller's reply.
|
|
65
|
+
if (handoff && restoreText) {
|
|
66
|
+
patch.promptRestore = {
|
|
67
|
+
id: `restore-${Date.now()}-${[...idSet].join(',')}`,
|
|
68
|
+
ids: [...idSet].map(String),
|
|
69
|
+
text: restoreText,
|
|
70
|
+
device: restoreState.device,
|
|
71
|
+
at: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
84
74
|
if (restoreText) patch.promptHistoryList = promptHistoryWithout(getState().promptHistoryList, restoreText);
|
|
85
75
|
if (idSet.size > 0) {
|
|
86
76
|
const items = getState().items.filter((item) => !idSet.has(item?.id));
|
|
@@ -127,7 +117,18 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
|
|
|
127
117
|
// interrupting should just cancel the running turn and let the steering
|
|
128
118
|
// prompt run next, NOT resurrect the in-flight prompt back into the draft.
|
|
129
119
|
const hasPendingSteering = pending.some((entry) => isQueuedEntryEditable(entry));
|
|
130
|
-
|
|
120
|
+
// A different device than the one that sent the prompt is cancelling. Only
|
|
121
|
+
// plain text can be handed over through the state; a prompt carrying
|
|
122
|
+
// attachments stays in the transcript instead of landing in the wrong
|
|
123
|
+
// composer.
|
|
124
|
+
const canceller = typeof options?.device === 'string' ? options.device : '';
|
|
125
|
+
const crossDevice = Boolean(canceller && restoreState?.device && canceller !== restoreState.device);
|
|
126
|
+
const hasAttachments = Boolean(restoreState?.pastedImages || restoreState?.pastedTexts || restoreState?.content);
|
|
127
|
+
const canRestore =
|
|
128
|
+
options?.restorePrompt !== false &&
|
|
129
|
+
restoreState?.restorable &&
|
|
130
|
+
!hasPendingSteering &&
|
|
131
|
+
!(crossDevice && hasAttachments);
|
|
131
132
|
const restoreText = canRestore ? restoreState.text : '';
|
|
132
133
|
const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
133
134
|
const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
|
|
@@ -151,7 +152,7 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
|
|
|
151
152
|
discardExecutionPendingResume?.(restoreState.discardExecutionPendingResumeKeys);
|
|
152
153
|
}
|
|
153
154
|
if ((restoreText || requeueEntries.length > 0) && aborted !== false) {
|
|
154
|
-
reclaimInFlightPrompt(restoreState, { restoreText, requeueEntries });
|
|
155
|
+
reclaimInFlightPrompt(restoreState, { restoreText, requeueEntries, handoff: crossDevice });
|
|
155
156
|
}
|
|
156
157
|
restoreState.restorable = false;
|
|
157
158
|
restoreState.requeueEntries = [];
|
|
@@ -163,16 +164,19 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
|
|
|
163
164
|
restorePastedTexts,
|
|
164
165
|
canRestore ? restoreState?.content : null
|
|
165
166
|
);
|
|
167
|
+
// The canceller never receives another device's prompt.
|
|
168
|
+
const handedOver = crossDevice && Boolean(restoreText) && aborted !== false;
|
|
166
169
|
return {
|
|
167
170
|
aborted,
|
|
168
|
-
restoreText,
|
|
171
|
+
restoreText: handedOver ? '' : restoreText,
|
|
169
172
|
...(restored.content ? { content: restored.content } : {}),
|
|
170
173
|
...(restored.notice ? { notice: restored.notice } : {}),
|
|
171
174
|
pastedImages: restored.pastedImages,
|
|
172
175
|
discardPastedImages,
|
|
173
176
|
pastedTexts: restored.pastedTexts,
|
|
174
177
|
discardPastedTexts,
|
|
175
|
-
restoredSubmissionIds:
|
|
178
|
+
restoredSubmissionIds:
|
|
179
|
+
restoreText && !handedOver ? (restoreState?.submittedIds || []).map(String).filter(Boolean) : [],
|
|
176
180
|
};
|
|
177
181
|
};
|
|
178
182
|
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
// Prompt submission: mint the intake
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Prompt submission: mint the intake and queue it at once, starting idle
|
|
2
|
+
// auto-clear first when the session is idle. Queued prompts are reclaimed by
|
|
3
|
+
// Esc through restoreQueued.
|
|
4
4
|
import { promptDisplayText } from '../../queue-helpers.mjs';
|
|
5
5
|
|
|
6
6
|
export function createSubmissionIntake(bag) {
|
|
7
7
|
const { runtime, nextId, flags, getState, enqueue, autoClearBeforeSubmit } = bag;
|
|
8
|
-
// submitAsync may be awaiting auto-clear while the renderer already owns an
|
|
9
|
-
// optimistic user row. Keep that intake addressable so Esc can reclaim it
|
|
10
|
-
// before enqueue()/busy publication without racing a delayed snapshot.
|
|
11
|
-
const acceptingSubmissions = new Map();
|
|
12
8
|
|
|
13
9
|
const submission = (text, options = {}) => {
|
|
14
10
|
const displayText = promptDisplayText(text, options);
|
|
@@ -25,20 +21,17 @@ export function createSubmissionIntake(bag) {
|
|
|
25
21
|
displayText,
|
|
26
22
|
},
|
|
27
23
|
};
|
|
28
|
-
acceptingSubmissions.set(intake.queueOptions.id, intake);
|
|
29
24
|
return intake;
|
|
30
25
|
};
|
|
31
26
|
|
|
32
27
|
const enqueueSubmission = (intake) => {
|
|
33
|
-
const submissionId = String(intake?.queueOptions?.id || '').trim();
|
|
34
|
-
if (submissionId) acceptingSubmissions.delete(submissionId);
|
|
35
|
-
if (intake?.cancelled === true) return false;
|
|
36
28
|
const accepted = enqueue(intake.text, intake.queueOptions);
|
|
37
29
|
// User input wakes a passive `task wait` without cancelling either the
|
|
38
30
|
// turn or the background task. The returned running snapshot creates the
|
|
39
31
|
// normal post-tool boundary, where this queued prompt is injected.
|
|
40
32
|
if (accepted !== false) {
|
|
41
33
|
if ((intake.queueOptions.mode || 'prompt') === 'prompt') {
|
|
34
|
+
bag.cancelQuotaResumeOnUserInput?.();
|
|
42
35
|
bag.cancelQueuedGoalContinuations?.();
|
|
43
36
|
bag.archiveCompletedGoalOnUserInput?.();
|
|
44
37
|
}
|
|
@@ -58,18 +51,17 @@ export function createSubmissionIntake(bag) {
|
|
|
58
51
|
if (flags.autoClearRunning || getState().commandBusy || getState().busy) {
|
|
59
52
|
return enqueueSubmission(intake) !== false;
|
|
60
53
|
}
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// runs
|
|
64
|
-
//
|
|
65
|
-
|
|
54
|
+
// Start idle auto-clear first so commandBusy is raised synchronously, then
|
|
55
|
+
// queue right away: drain stays blocked until the clear settles, so the
|
|
56
|
+
// prompt still runs against the cleared conversation, and a later submit
|
|
57
|
+
// can never be queued ahead of this one. A rejection or synchronous throw
|
|
58
|
+
// must never lose the prompt.
|
|
66
59
|
try {
|
|
67
|
-
|
|
60
|
+
void Promise.resolve(autoClearBeforeSubmit()).catch(() => {});
|
|
68
61
|
} catch {
|
|
69
|
-
|
|
62
|
+
/* the prompt is queued below regardless */
|
|
70
63
|
}
|
|
71
|
-
|
|
72
|
-
return true;
|
|
64
|
+
return enqueueSubmission(intake) !== false;
|
|
73
65
|
};
|
|
74
66
|
|
|
75
67
|
const submitAsync = async (text, options = {}) => {
|
|
@@ -107,5 +99,5 @@ export function createSubmissionIntake(bag) {
|
|
|
107
99
|
return await settled;
|
|
108
100
|
};
|
|
109
101
|
|
|
110
|
-
return {
|
|
102
|
+
return { submit, submitAsync, submitAndWait };
|
|
111
103
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* intake.mjs — the session object's prompt intake and interruption surface:
|
|
3
3
|
* state access/subscription and the reserved-session handshake here, with
|
|
4
4
|
* submit / submitAsync / submitAndWait, abort (which reclaims an in-flight or
|
|
5
|
-
*
|
|
5
|
+
* queued submission back into the draft) and message rewind composed
|
|
6
6
|
* from ./intake/.
|
|
7
7
|
*/
|
|
8
8
|
import { createAbortAction } from './intake/abort.mjs';
|
|
@@ -24,7 +24,7 @@ export function createSessionIntakeApi(bag) {
|
|
|
24
24
|
restoreQueued,
|
|
25
25
|
prioritizeQueued,
|
|
26
26
|
} = bag;
|
|
27
|
-
const {
|
|
27
|
+
const { submit, submitAsync, submitAndWait } = createSubmissionIntake(bag);
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
30
|
getState: () => getPublishedState(),
|
|
@@ -49,6 +49,6 @@ export function createSessionIntakeApi(bag) {
|
|
|
49
49
|
restoreQueued,
|
|
50
50
|
prioritizeQueued,
|
|
51
51
|
...createRewindAction(bag),
|
|
52
|
-
...createAbortAction(bag
|
|
52
|
+
...createAbortAction(bag),
|
|
53
53
|
};
|
|
54
54
|
}
|