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
|
@@ -47,7 +47,8 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
|
|
|
47
47
|
// though the session runtime implements it.
|
|
48
48
|
return await requireRuntimeMethod('consumeCodexRateLimitResetCredit', 'Codex reset is unavailable')(options);
|
|
49
49
|
},
|
|
50
|
-
getTurnReviewDiff: (options = {}) =>
|
|
50
|
+
getTurnReviewDiff: (options = {}) =>
|
|
51
|
+
turnReviewReply((reviewOptions) => runtime.getTurnReviewDiff?.(reviewOptions), options),
|
|
51
52
|
getSessionReviewDiff: async () => {
|
|
52
53
|
return (await runtime.getSessionReviewDiff?.()) ?? { supported: false, files: [], patch: '' };
|
|
53
54
|
},
|
|
@@ -106,6 +107,7 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
|
|
|
106
107
|
(input) => runtime.saveCustomProvider(input),
|
|
107
108
|
(result) => `custom provider saved: ${result.id}`
|
|
108
109
|
),
|
|
110
|
+
updateCustomProviderModel: (id, modelId, patch) => runtime.updateCustomProviderModel(id, modelId, patch),
|
|
109
111
|
removeCustomProvider: async (id) => {
|
|
110
112
|
const result = await runtime.removeCustomProvider(id);
|
|
111
113
|
refreshRouteStats();
|
|
@@ -173,5 +175,7 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
|
|
|
173
175
|
(name, enabled) => runtime.setWebhookEnabled(name, enabled),
|
|
174
176
|
(_result, name, enabled) => `webhook ${enabled ? 'enabled' : 'disabled'}: ${name}`
|
|
175
177
|
),
|
|
178
|
+
// Signed test request only; nothing changes, so no announcement.
|
|
179
|
+
testWebhook: (name) => runtime.testWebhook(name),
|
|
176
180
|
};
|
|
177
181
|
}
|
|
@@ -12,8 +12,8 @@ export function createInheritanceActions(bag, { restoreTranscriptItems }) {
|
|
|
12
12
|
* daemon resolves actions on THIS surface, so `inheritFrom` has to live here
|
|
13
13
|
* and not only on the runtime beneath it.
|
|
14
14
|
*/
|
|
15
|
-
const inheritFrom = async (sourceSessionId) => {
|
|
16
|
-
const result = await runtime.inheritFrom(sourceSessionId);
|
|
15
|
+
const inheritFrom = async (sourceSessionId, options = null) => {
|
|
16
|
+
const result = await runtime.inheritFrom(sourceSessionId, options);
|
|
17
17
|
const sessionId = String(result?.sessionId || runtime.sessionId || getState().sessionId || '');
|
|
18
18
|
// Only model messages travel with the conversation. Rebuild the visible
|
|
19
19
|
// transcript from them right here so the heir opens showing the carried
|
|
@@ -80,6 +80,9 @@ export function createSessionResetActions(bag) {
|
|
|
80
80
|
return {
|
|
81
81
|
clear: () =>
|
|
82
82
|
withResetLock(async () => {
|
|
83
|
+
const clearedId = getState().sessionId;
|
|
84
|
+
// A cleared session has no failed turn left to resume.
|
|
85
|
+
bag.supersedeQuotaResume?.(clearedId);
|
|
83
86
|
const rollbackSnapshot = beginTuiReset();
|
|
84
87
|
set(blankSessionPatch());
|
|
85
88
|
try {
|
|
@@ -101,6 +104,7 @@ export function createSessionResetActions(bag) {
|
|
|
101
104
|
if (rollbackSnapshot) restoreTuiAfterFailedSessionReset(rollbackSnapshot);
|
|
102
105
|
return false;
|
|
103
106
|
}
|
|
107
|
+
bag.supersedeQuotaResume?.(id);
|
|
104
108
|
if (deletingCurrent) commitEmptySession(rollbackSnapshot, { sessionId: null, cwd: runtime.cwd });
|
|
105
109
|
return true;
|
|
106
110
|
} catch (error) {
|
|
@@ -12,10 +12,51 @@ import { createDisposeAction } from './lifecycle/dispose.mjs';
|
|
|
12
12
|
export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFlows }) {
|
|
13
13
|
const { runtime, pushNotice, removeNotice, setProgressHint, finishToolApproval } = bag;
|
|
14
14
|
|
|
15
|
+
// Canonical close = execution fence at the local-session boundary. The fence
|
|
16
|
+
// is raised synchronously BEFORE the runtime/manager close (which cancels the
|
|
17
|
+
// session's shells and so triggers completion notifications), so no queue
|
|
18
|
+
// drain, completion wake, pending-resume kick or new submission can start
|
|
19
|
+
// work for a conversation that is being closed. A durable close that fails
|
|
20
|
+
// restores admission with every accepted input intact; one that succeeds
|
|
21
|
+
// leaves the session terminal and settles queued submissions explicitly.
|
|
22
|
+
function closeCanonical(reason) {
|
|
23
|
+
const flags = bag.flags || {};
|
|
24
|
+
const previous = flags.admission || 'open';
|
|
25
|
+
if (previous === 'terminal') return runtime.closeCanonicalSession?.(reason) === true;
|
|
26
|
+
flags.admission = 'closing';
|
|
27
|
+
let closed = false;
|
|
28
|
+
try {
|
|
29
|
+
closed = runtime.closeCanonicalSession?.(reason) === true;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
flags.admission = previous;
|
|
32
|
+
restoreAdmission();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
if (!closed) {
|
|
36
|
+
flags.admission = previous;
|
|
37
|
+
restoreAdmission();
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
flags.admission = 'terminal';
|
|
41
|
+
flags.admissionGeneration = (Number(flags.admissionGeneration) || 0) + 1;
|
|
42
|
+
bag.settleClosedQueue?.();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function restoreAdmission() {
|
|
47
|
+
void bag.drain?.();
|
|
48
|
+
bag.flushDeferredExecutionPendingResumeKick?.();
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
return {
|
|
16
52
|
resolveToolApproval: (id, decision = {}) => {
|
|
17
53
|
const approved = decision === true || decision?.approved === true;
|
|
18
|
-
return finishToolApproval(
|
|
54
|
+
return finishToolApproval(
|
|
55
|
+
id,
|
|
56
|
+
approved,
|
|
57
|
+
decision?.reason || (approved ? 'approved by user' : 'denied by user'),
|
|
58
|
+
typeof decision?.device === 'string' ? decision.device : ''
|
|
59
|
+
);
|
|
19
60
|
},
|
|
20
61
|
pushNotice,
|
|
21
62
|
removeNotice,
|
|
@@ -27,6 +68,32 @@ export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFl
|
|
|
27
68
|
renameSessionTitle: (id, title) => {
|
|
28
69
|
return runtime.renameSessionTitle?.(id, title) ?? false;
|
|
29
70
|
},
|
|
71
|
+
// Quota escape hatch: re-route THIS session onto another model, in place,
|
|
72
|
+
// only after its latest settled turn failed on exhausted provider quota.
|
|
73
|
+
// Starts no turn — the caller resubmits through retryFailedTurn.
|
|
74
|
+
continueSessionInModel: async (sessionId, route) => {
|
|
75
|
+
const state = bag.getState();
|
|
76
|
+
if (state.busy || state.commandBusy) throw new Error('session is busy');
|
|
77
|
+
if (!sessionId || state.sessionId !== sessionId) throw new Error('session is not the active session');
|
|
78
|
+
const items = Array.isArray(state.items) ? state.items : [];
|
|
79
|
+
const last = items.findLast((item) => item?.kind === 'user' || item?.kind === 'turndone');
|
|
80
|
+
if (last?.kind !== 'turndone' || last.status !== 'failed' || !last.quota) {
|
|
81
|
+
throw new Error('latest turn did not fail on exhausted provider quota');
|
|
82
|
+
}
|
|
83
|
+
if (typeof runtime.continueSessionInModel !== 'function') throw new Error('runtime cannot continue in a model');
|
|
84
|
+
// Continuing elsewhere supersedes waiting for this provider's reset.
|
|
85
|
+
bag.supersedeQuotaResume?.(sessionId);
|
|
86
|
+
bag.set({ commandBusy: true });
|
|
87
|
+
try {
|
|
88
|
+
await runtime.continueSessionInModel(sessionId, route);
|
|
89
|
+
bag.set({ ...bag.routeState(), stats: { ...bag.getState().stats } });
|
|
90
|
+
bag.syncContextStats?.({ allowEstimated: true });
|
|
91
|
+
return true;
|
|
92
|
+
} finally {
|
|
93
|
+
bag.set({ commandBusy: false });
|
|
94
|
+
bag.flushEmitImmediate?.();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
30
97
|
// Desktop sidebar watcher hook: the daemon watches this directory so
|
|
31
98
|
// heartbeat sidecar create/delete pushes instant working/dot updates.
|
|
32
99
|
// Without it the watcher silently no-ops and the sidebar falls back to
|
|
@@ -44,7 +111,9 @@ export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFl
|
|
|
44
111
|
deliverToolCompletion: (sessionId, text, meta = {}) =>
|
|
45
112
|
runtime.deliverToolCompletion?.(sessionId, text, meta) === true,
|
|
46
113
|
|
|
47
|
-
closeCanonicalSession: (reason = 'canonical-session-close') =>
|
|
114
|
+
closeCanonicalSession: (reason = 'canonical-session-close') => closeCanonical(reason),
|
|
115
|
+
|
|
116
|
+
bindSessionAddress: (sessionId) => runtime.bindSessionAddress?.(sessionId) === true,
|
|
48
117
|
|
|
49
118
|
...createDisposeAction(bag, { oauthFlows }),
|
|
50
119
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* claims, the TUI theme, and the media studio passthroughs.
|
|
5
5
|
*/
|
|
6
6
|
import { listThemes, getThemeSetting, setThemeSetting } from '../../theme.mjs';
|
|
7
|
-
import { getVoiceStatus, toggleVoice } from '
|
|
7
|
+
import { getVoiceStatus, toggleVoice } from '../../../runtime/channels/lib/voice-setup.mjs';
|
|
8
8
|
|
|
9
9
|
const AUDIO_EXTENSIONS = [
|
|
10
10
|
[/ogg/i, 'ogg'],
|
|
@@ -98,7 +98,7 @@ export function createSessionMediaApi(bag) {
|
|
|
98
98
|
const result = await toggleVoice({
|
|
99
99
|
pushNotice,
|
|
100
100
|
setProgressHint,
|
|
101
|
-
enabled
|
|
101
|
+
enabled,
|
|
102
102
|
});
|
|
103
103
|
return {
|
|
104
104
|
...(await getVoiceStatus()),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quota-resume.mjs — the session object's quota auto-resume surface: the
|
|
3
|
+
* session-addressed controls (schedule/cancel the resume, retry a failed
|
|
4
|
+
* turn), the daemon scheduler's fire entry, and the global auto-resume
|
|
5
|
+
* setting. The lane behind them is ../quota-resume.mjs.
|
|
6
|
+
*/
|
|
7
|
+
import { quotaAutoResumeEnabled, setQuotaAutoResumeEnabled } from '../../../runtime/shared/quota-resume-store.mjs';
|
|
8
|
+
|
|
9
|
+
export function createQuotaResumeApi(bag) {
|
|
10
|
+
return {
|
|
11
|
+
scheduleQuotaResume: (sessionId) => bag.scheduleQuotaResume(sessionId),
|
|
12
|
+
cancelQuotaResume: (sessionId) => bag.cancelQuotaResume(sessionId),
|
|
13
|
+
retryFailedTurn: (sessionId, turnKey) => bag.retryFailedTurn(sessionId, turnKey),
|
|
14
|
+
fireQuotaResume: (options) => bag.fireQuotaResume(options),
|
|
15
|
+
getQuotaAutoResume: () => quotaAutoResumeEnabled(),
|
|
16
|
+
setQuotaAutoResume: (enabled) => {
|
|
17
|
+
if (typeof enabled !== 'boolean') throw new TypeError('quota auto resume must be boolean');
|
|
18
|
+
return setQuotaAutoResumeEnabled(enabled);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -61,6 +61,8 @@ export function createSessionRouteApi(bag) {
|
|
|
61
61
|
|
|
62
62
|
const setRoute = async (opts) => {
|
|
63
63
|
if (getState().commandBusy && !routeWrite) return false;
|
|
64
|
+
// Any route change supersedes waiting for the old route's quota reset.
|
|
65
|
+
bag.supersedeQuotaResume?.(getState().sessionId);
|
|
64
66
|
const token = ++routeSequence;
|
|
65
67
|
const previousWrite = routeWrite;
|
|
66
68
|
const routeOpts = opts && typeof opts === 'object' ? opts : {};
|
|
@@ -11,6 +11,7 @@ import { createSessionSettingsApi } from './session-api/settings.mjs';
|
|
|
11
11
|
import { createSessionControlsApi } from './session-api/controls.mjs';
|
|
12
12
|
import { createSessionExtensionsApi } from './session-api/extensions.mjs';
|
|
13
13
|
import { createSessionCommandsApi } from './session-api/commands.mjs';
|
|
14
|
+
import { createQuotaResumeApi } from './session-api/quota-resume.mjs';
|
|
14
15
|
|
|
15
16
|
export function createSessionApi(bag) {
|
|
16
17
|
const api = { ...createSessionApiA(bag), ...createSessionApiB(bag) };
|
|
@@ -57,5 +58,6 @@ export function createSessionApiA(bag) {
|
|
|
57
58
|
...createSessionControlsApi(bag),
|
|
58
59
|
...createSessionExtensionsApi(bag),
|
|
59
60
|
...createSessionCommandsApi(bag),
|
|
61
|
+
...createQuotaResumeApi(bag),
|
|
60
62
|
};
|
|
61
63
|
}
|
|
@@ -76,16 +76,20 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
|
|
|
76
76
|
// Never forces busy=false mid-turn: bails while a turn is in flight.
|
|
77
77
|
function flushDeferredClearedSessionUi() {
|
|
78
78
|
if (!flags.pendingClearedSessionUi || getState().busy) return;
|
|
79
|
-
const { result } = flags.pendingClearedSessionUi;
|
|
79
|
+
const { result, sessionId } = flags.pendingClearedSessionUi;
|
|
80
80
|
flags.pendingClearedSessionUi = null;
|
|
81
81
|
flags.autoClearInFlight = false;
|
|
82
|
+
if (runtime.session?.id !== sessionId) return;
|
|
82
83
|
if (applyAutoClearUi(result)) pushNotice(LATE_COMPLETION_NOTICE, 'info');
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function onLateCompact(lateResult) {
|
|
86
|
+
function onLateCompact(lateResult, sessionId) {
|
|
87
|
+
// The active session changed while the abandoned compaction ran: its
|
|
88
|
+
// result belongs to a session that is no longer shown.
|
|
89
|
+
if (runtime.session?.id !== sessionId) return;
|
|
86
90
|
if (getState().busy) {
|
|
87
91
|
// Do not wipe items/queued or force busy=false mid-turn.
|
|
88
|
-
flags.pendingClearedSessionUi = { result: lateResult };
|
|
92
|
+
flags.pendingClearedSessionUi = { result: lateResult, sessionId };
|
|
89
93
|
} else {
|
|
90
94
|
if (applyAutoClearUi(lateResult)) pushNotice(LATE_COMPLETION_NOTICE, 'info');
|
|
91
95
|
}
|
|
@@ -94,7 +98,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
|
|
|
94
98
|
// Validate both rejected calls and error results before either normal or
|
|
95
99
|
// late completion can reset the UI. Never follow compact with clear:
|
|
96
100
|
// that would discard conversation/execution records the compactor kept.
|
|
97
|
-
async function compactWithinTimeout(compactTimeoutMs) {
|
|
101
|
+
async function compactWithinTimeout(compactTimeoutMs, sessionId) {
|
|
98
102
|
const compactPromise = runtime.compact({ requireReduction: true }).then((result) => {
|
|
99
103
|
if (!result) throw new Error('no active session');
|
|
100
104
|
if (result.error) throw new Error(result.error);
|
|
@@ -112,7 +116,10 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
|
|
|
112
116
|
} catch (raceError) {
|
|
113
117
|
flags.autoClearInFlight = true;
|
|
114
118
|
compactPromise
|
|
115
|
-
.then(
|
|
119
|
+
.then(
|
|
120
|
+
(lateResult) => onLateCompact(lateResult, sessionId),
|
|
121
|
+
() => {}
|
|
122
|
+
)
|
|
116
123
|
.finally(() => {
|
|
117
124
|
if (!flags.pendingClearedSessionUi) flags.autoClearInFlight = false;
|
|
118
125
|
});
|
|
@@ -146,6 +153,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
|
|
|
146
153
|
// results without a summary. Plain /clear remains a separate explicit wipe.
|
|
147
154
|
async function performAutoClear({ compactTimeoutMs = AUTO_CLEAR_COMPACT_TIMEOUT_MS } = {}) {
|
|
148
155
|
flags.autoClearRunning = true;
|
|
156
|
+
const sessionId = runtime.session?.id;
|
|
149
157
|
const startedAt = Date.now();
|
|
150
158
|
// commandBusy blocks concurrent session commands (resume/newSession/
|
|
151
159
|
// setModel) AND new submits for the duration of the async clear — the
|
|
@@ -161,7 +169,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
|
|
|
161
169
|
// Without this, long idle clears can look like a frozen prompt followed by
|
|
162
170
|
// an already-complete status row.
|
|
163
171
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
164
|
-
return applyAutoClearUi(await compactWithinTimeout(compactTimeoutMs));
|
|
172
|
+
return applyAutoClearUi(await compactWithinTimeout(compactTimeoutMs, sessionId));
|
|
165
173
|
} catch (error) {
|
|
166
174
|
const message = presentErrorText(error, { surface: 'compact' });
|
|
167
175
|
pushItem({
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
mergePastedImages,
|
|
13
13
|
mergePastedTexts,
|
|
14
14
|
} from '../queue-helpers.mjs';
|
|
15
|
-
import { appendTuiSteeringPersist } from '../tui-steering-persist.mjs';
|
|
15
|
+
import { appendTuiSteeringPersist, dropTuiSteeringPersist } from '../tui-steering-persist.mjs';
|
|
16
16
|
import { parseModelVisibleCompletionWrapper } from '../agent-envelope.mjs';
|
|
17
17
|
import { rewoundFailedTurnItems } from './failed-turn-rewind.mjs';
|
|
18
18
|
|
|
@@ -55,9 +55,11 @@ function renderBatchItems(batch, pushUserOrSyntheticItem) {
|
|
|
55
55
|
const sender = String(entry.transcriptMeta?.sender || '')
|
|
56
56
|
.trim()
|
|
57
57
|
.toLowerCase();
|
|
58
|
-
|
|
58
|
+
const device = typeof entry.transcriptMeta?.device === 'string' ? entry.transcriptMeta.device : '';
|
|
59
|
+
const attribution = { ...(sender ? { sender } : {}), ...(device ? { device } : {}) };
|
|
60
|
+
let itemExtras = sender || device ? attribution : null;
|
|
59
61
|
if (Array.isArray(entry.images) && entry.images.length) {
|
|
60
|
-
itemExtras = { images: entry.images, ...
|
|
62
|
+
itemExtras = { images: entry.images, ...attribution };
|
|
61
63
|
}
|
|
62
64
|
pushUserOrSyntheticItem(entry.text, entry.id, isQueuedEntryEditable(entry) ? 'user' : 'injected', itemExtras);
|
|
63
65
|
}
|
|
@@ -160,7 +162,41 @@ export function createDrainLoop(bag, { queue, steering, submissions, flushDeferr
|
|
|
160
162
|
return pending.some((entry) => !isSlashQueuedEntry(entry));
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
const admission = () => flags.admission || 'open';
|
|
166
|
+
|
|
167
|
+
// Canonical close settled: nothing queued can ever run. Every accepted
|
|
168
|
+
// submission is settled explicitly (rejected), never silently dropped, and
|
|
169
|
+
// its durable steering mirror is released so a restart cannot resurrect it
|
|
170
|
+
// into the closed conversation.
|
|
171
|
+
function settleClosedQueue() {
|
|
172
|
+
const entries = pending.splice(0);
|
|
173
|
+
pendingNotificationKeys.clear();
|
|
174
|
+
clearBlockedDrainRetry();
|
|
175
|
+
if (entries.length === 0) return;
|
|
176
|
+
queue.removeQueuedEntries(entries);
|
|
177
|
+
const mirrored = entries.filter(
|
|
178
|
+
(entry) => steering.shouldMirrorSteeringEntry(entry) && !entry.steeringPersistRestored
|
|
179
|
+
);
|
|
180
|
+
if (mirrored.length > 0) {
|
|
181
|
+
void Promise.resolve(dropTuiSteeringPersist(steering.leadSessionId(), mirrored)).catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (entry.id) submissions.forget(entry.id);
|
|
185
|
+
try {
|
|
186
|
+
entry.onSettled?.({ status: 'rejected', reason: 'session-closed', result: null, session: null });
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
163
191
|
async function drain() {
|
|
192
|
+
// Execution admission fence: a closing session starts nothing (the close
|
|
193
|
+
// path re-kicks the queue if the durable close fails); a closed one
|
|
194
|
+
// settles what was queued.
|
|
195
|
+
if (admission() === 'closing') return;
|
|
196
|
+
if (admission() === 'terminal') {
|
|
197
|
+
settleClosedQueue();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
164
200
|
if (flags.draining) return;
|
|
165
201
|
// Bail while any session command holds commandBusy (auto-clear implies it,
|
|
166
202
|
// but so do setModel/newSession/resume/etc). Running a turn concurrently
|
|
@@ -196,6 +232,7 @@ export function createDrainLoop(bag, { queue, steering, submissions, flushDeferr
|
|
|
196
232
|
try {
|
|
197
233
|
while (pending.length > 0) {
|
|
198
234
|
if (flags.drainEpoch !== drainEpoch) return;
|
|
235
|
+
if (admission() !== 'open') return;
|
|
199
236
|
// Drain one priority/mode bucket at a time (unified command queue):
|
|
200
237
|
// prompt steering stays editable and task notifications stay
|
|
201
238
|
// non-editable but model-visible.
|
|
@@ -230,13 +267,23 @@ export function createDrainLoop(bag, { queue, steering, submissions, flushDeferr
|
|
|
230
267
|
if (flags.drainEpoch === drainEpoch) {
|
|
231
268
|
flags.draining = false;
|
|
232
269
|
flushDeferredClearedSessionUi();
|
|
233
|
-
if (
|
|
270
|
+
if (admission() === 'terminal') settleClosedQueue();
|
|
271
|
+
else if (hasModelDrainablePending()) void drain();
|
|
234
272
|
else flushDeferredExecutionPendingResumeKick();
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
}
|
|
238
276
|
|
|
239
277
|
function enqueue(text, options = {}) {
|
|
278
|
+
// Admission fence. While closing, only user prompts are accepted (retained
|
|
279
|
+
// so a failed close loses nothing); notification-driven/model-visible
|
|
280
|
+
// enqueues are refused. A closed session accepts nothing.
|
|
281
|
+
const state = admission();
|
|
282
|
+
const mode = options.mode || 'prompt';
|
|
283
|
+
if (state !== 'open' && (state === 'terminal' || mode !== 'prompt')) {
|
|
284
|
+
tuiDebug(`enqueue refused: session admission=${state} mode=${mode}`);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
240
287
|
const submissionId = String(options.id || '').trim();
|
|
241
288
|
// Idempotent intake: a transport that re-sent this exact submission (its
|
|
242
289
|
// first attempt landed but the response was lost) must not double-post.
|
|
@@ -282,5 +329,5 @@ export function createDrainLoop(bag, { queue, steering, submissions, flushDeferr
|
|
|
282
329
|
return true;
|
|
283
330
|
}
|
|
284
331
|
|
|
285
|
-
return { drain, enqueue };
|
|
332
|
+
return { drain, enqueue, settleClosedQueue };
|
|
286
333
|
}
|
|
@@ -79,10 +79,9 @@ export function createTakeEntriesOps({ pending, pendingNotificationKeys, removeQ
|
|
|
79
79
|
.filter((text) => String(text || '').trim())
|
|
80
80
|
.join('\n');
|
|
81
81
|
const combinedText = [queuedText, String(currentText || '')].filter((text) => text.trim()).join('\n');
|
|
82
|
+
const verb = unreadable === 1 ? ' was' : 's were';
|
|
82
83
|
return {
|
|
83
|
-
...(unreadable
|
|
84
|
-
? { notice: `${unreadable} attachment${unreadable === 1 ? ' was' : 's were'} no longer available and dropped.` }
|
|
85
|
-
: {}),
|
|
84
|
+
...(unreadable ? { notice: `${unreadable} attachment${verb} no longer available and dropped.` } : {}),
|
|
86
85
|
count: queued.length,
|
|
87
86
|
ids: queued.map((item) => String(item.id || '')).filter(Boolean),
|
|
88
87
|
text: combinedText,
|
|
@@ -70,6 +70,9 @@ export function createQueueOps(bag, { kickDrain }) {
|
|
|
70
70
|
pending.unshift(...restored);
|
|
71
71
|
const visible = restored.filter(isQueuedEntryVisible);
|
|
72
72
|
if (visible.length > 0) set({ queued: [...visible, ...getState().queued] });
|
|
73
|
+
// Requeued from a turn that settled after the canonical close: settle
|
|
74
|
+
// explicitly instead of parking work no drain will ever run.
|
|
75
|
+
if (bag.flags?.admission === 'terminal') bag.settleClosedQueue?.();
|
|
73
76
|
return true;
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -116,7 +116,12 @@ export function createSteeringOps(bag, { queue, submissions }) {
|
|
|
116
116
|
return !isGoalQueuedEntry(entry) || bag.shouldRunGoalContinuation?.(entry) === true;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Pre-send steering is an admission route: a closing/closed session injects
|
|
120
|
+
// nothing into a model request.
|
|
121
|
+
const admissionOpen = () => !bag.flags?.admission || bag.flags.admission === 'open';
|
|
122
|
+
|
|
119
123
|
function hasPendingSteering() {
|
|
124
|
+
if (!admissionOpen()) return false;
|
|
120
125
|
return pending.some(
|
|
121
126
|
(entry) =>
|
|
122
127
|
isSteerableEntry(entry) &&
|
|
@@ -134,6 +139,7 @@ export function createSteeringOps(bag, { queue, submissions }) {
|
|
|
134
139
|
else if (_sessionIdOrOptions && typeof _sessionIdOrOptions === 'object') options = _sessionIdOrOptions;
|
|
135
140
|
const maxPriority = options.maxPriority || 'next';
|
|
136
141
|
const out = [];
|
|
142
|
+
if (!admissionOpen()) return out;
|
|
137
143
|
for (;;) {
|
|
138
144
|
const batch = queue.dequeueQueueBatch(maxPriority, { predicate: isSteerableEntry });
|
|
139
145
|
if (batch.length === 0) break;
|
|
@@ -156,6 +162,8 @@ export function createSteeringOps(bag, { queue, submissions }) {
|
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
async function restoreLeadSteeringFromDisk() {
|
|
165
|
+
// A closed/closing conversation never gets queue work restored into it.
|
|
166
|
+
if (!admissionOpen()) return;
|
|
159
167
|
const rows = await drainTuiSteeringPersist(leadSessionId());
|
|
160
168
|
if (!rows.length) return;
|
|
161
169
|
const livePersistIds = new Set(pending.map((entry) => entry?.steeringPersistId).filter(Boolean));
|
|
@@ -190,6 +198,9 @@ export function createSteeringOps(bag, { queue, submissions }) {
|
|
|
190
198
|
if (isQueuedEntryVisible(entry)) restored.push(entry);
|
|
191
199
|
}
|
|
192
200
|
if (restored.length > 0) set({ queued: [...getState().queued, ...restored] });
|
|
201
|
+
// The conversation closed while the rows were being read: settle them
|
|
202
|
+
// explicitly (rejected + durable mirror released), never leave them parked.
|
|
203
|
+
if (bag.flags?.admission === 'terminal') bag.settleClosedQueue?.();
|
|
193
204
|
if (droppedDelivered > 0) {
|
|
194
205
|
try {
|
|
195
206
|
process.stderr.write(`[tui] skipped ${droppedDelivered} already-delivered steering row(s) on restore\n`);
|
|
@@ -36,6 +36,7 @@ export function createSessionFlow(bag) {
|
|
|
36
36
|
dequeueQueueBatch: queue.dequeueQueueBatch,
|
|
37
37
|
drain: loop.drain,
|
|
38
38
|
enqueue: loop.enqueue,
|
|
39
|
+
settleClosedQueue: loop.settleClosedQueue,
|
|
39
40
|
hasPendingSteering: steering.hasPendingSteering,
|
|
40
41
|
drainPendingSteering: steering.drainPendingSteering,
|
|
41
42
|
restoreLeadSteeringFromDisk: steering.restoreLeadSteeringFromDisk,
|
|
@@ -56,12 +56,17 @@ export function createToolApproval({ getState, set, nextId, getDisposed, timeout
|
|
|
56
56
|
armToolApprovalTimeout(entry);
|
|
57
57
|
set({ toolApproval: entry.request });
|
|
58
58
|
}
|
|
59
|
-
function finishToolApproval(id, approved, reason = '') {
|
|
59
|
+
function finishToolApproval(id, approved, reason = '', device = '') {
|
|
60
60
|
const targetId = String(id || '');
|
|
61
61
|
if (activeToolApproval && activeToolApproval.id === targetId) {
|
|
62
62
|
const entry = activeToolApproval;
|
|
63
63
|
activeToolApproval = null;
|
|
64
|
-
|
|
64
|
+
// Only an answer from a known device is attributed; a timeout or an
|
|
65
|
+
// interrupt names nobody.
|
|
66
|
+
const toolApprovalResult = device
|
|
67
|
+
? { id: targetId, approved: approved === true, device: String(device), at: Date.now() }
|
|
68
|
+
: null;
|
|
69
|
+
set({ toolApproval: null, toolApprovalResult });
|
|
65
70
|
settleToolApproval(entry, approved === true, String(reason || ''));
|
|
66
71
|
presentNextToolApproval();
|
|
67
72
|
return true;
|
|
@@ -109,6 +109,7 @@ export function createTranscriptIntake({ getState, flags, pushItem, patchItem, t
|
|
|
109
109
|
text,
|
|
110
110
|
...transcriptMeta,
|
|
111
111
|
...(extra && typeof extra.sender === 'string' && extra.sender ? { sender: extra.sender } : {}),
|
|
112
|
+
...(extra && typeof extra.device === 'string' && extra.device ? { device: extra.device } : {}),
|
|
112
113
|
// Byte-free attachment metadata (name/mime/size) from the queue entry —
|
|
113
114
|
// lets the desktop transcript render image chips without ever carrying
|
|
114
115
|
// base64 payloads through snapshots.
|
package/src/tui/session/turn.mjs
CHANGED
|
@@ -152,6 +152,8 @@ function promptRestoreFor(displayText, options, submittedIds, content = null) {
|
|
|
152
152
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
153
153
|
restorable: options.restorable !== false,
|
|
154
154
|
submittedIds,
|
|
155
|
+
// Which device submitted the prompt: only that device may get it back.
|
|
156
|
+
device: typeof options.transcriptMeta?.device === 'string' ? options.transcriptMeta.device : '',
|
|
155
157
|
reclaimed: false,
|
|
156
158
|
committed: false,
|
|
157
159
|
requeueEntries: Array.isArray(options.requeueOnAbort) ? options.requeueOnAbort.slice() : [],
|
|
@@ -190,6 +192,9 @@ function steeringItemExtras(steeringMeta) {
|
|
|
190
192
|
...(typeof steeringMeta?.transcriptMeta?.sender === 'string' && steeringMeta.transcriptMeta.sender
|
|
191
193
|
? { sender: steeringMeta.transcriptMeta.sender }
|
|
192
194
|
: {}),
|
|
195
|
+
...(typeof steeringMeta?.transcriptMeta?.device === 'string' && steeringMeta.transcriptMeta.device
|
|
196
|
+
? { device: steeringMeta.transcriptMeta.device }
|
|
197
|
+
: {}),
|
|
193
198
|
};
|
|
194
199
|
}
|
|
195
200
|
|
|
@@ -389,7 +394,7 @@ function completeTurn(ctx, result, session) {
|
|
|
389
394
|
turn.finishedNormally = true;
|
|
390
395
|
}
|
|
391
396
|
|
|
392
|
-
function failTurn(ctx, error) {
|
|
397
|
+
export function failTurn(ctx, error) {
|
|
393
398
|
const { turn, stream, cards, pushNotice } = ctx;
|
|
394
399
|
flushStreamBatch(stream); // ensure any batched text lands before the error notice
|
|
395
400
|
if (isCancelLikeError(error)) {
|
|
@@ -401,12 +406,20 @@ function failTurn(ctx, error) {
|
|
|
401
406
|
turn.failed = true;
|
|
402
407
|
cards.aggregates.finalizeToolHeaders();
|
|
403
408
|
turn.usageLimited = isUsageLimitError(error);
|
|
409
|
+
const exhausted = error?.quotaExhausted;
|
|
410
|
+
if (exhausted && typeof exhausted === 'object') {
|
|
411
|
+
turn.quota = {
|
|
412
|
+
provider: String(exhausted.provider || turn.routeMeta?.provider || ''),
|
|
413
|
+
model: String(exhausted.model || turn.routeMeta?.model || ''),
|
|
414
|
+
resetAt: Number.isFinite(exhausted.resetAt) ? exhausted.resetAt : null,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
404
417
|
turn.failureDetail = toolErrorDisplay(error, 'turn').replace(/^Error:\s*/i, '');
|
|
405
418
|
turn.failureDiagnostic = safeErrorDetails(errText(error));
|
|
406
419
|
pushNotice(turn.failureDetail, 'error', { owner: 'transcript' });
|
|
407
420
|
}
|
|
408
421
|
|
|
409
|
-
function turnDoneItem({ turn, stream, cards, getState, nextId }) {
|
|
422
|
+
export function turnDoneItem({ turn, stream, cards, getState, nextId }) {
|
|
410
423
|
return {
|
|
411
424
|
kind: 'turndone',
|
|
412
425
|
id: nextId(),
|
|
@@ -425,6 +438,7 @@ function turnDoneItem({ turn, stream, cards, getState, nextId }) {
|
|
|
425
438
|
...(turn.failureDetail ? { detail: turn.failureDetail } : {}),
|
|
426
439
|
...(turn.failureDiagnostic ? { errorDetails: turn.failureDiagnostic } : {}),
|
|
427
440
|
...turn.routeMeta,
|
|
441
|
+
...(turn.failed && turn.quota ? { quota: turn.quota } : {}),
|
|
428
442
|
};
|
|
429
443
|
}
|
|
430
444
|
|
|
@@ -462,6 +476,14 @@ function publishTurnClose(ctx, closingItems) {
|
|
|
462
476
|
toolMode: runtime.toolMode,
|
|
463
477
|
...agentStatusState({ force: true }),
|
|
464
478
|
});
|
|
479
|
+
// Same frame as the failed turndone and busy=false: the resume is queued
|
|
480
|
+
// (and `quotaResume` published) before anything can observe the idle
|
|
481
|
+
// session, so a cancel at any later point finds it and wins.
|
|
482
|
+
if (turn.failed && turn.quota) {
|
|
483
|
+
try {
|
|
484
|
+
ctx.bag.beginQuotaSettlement?.();
|
|
485
|
+
} catch {}
|
|
486
|
+
}
|
|
465
487
|
ctx.flushDeferredExecutionPendingResumeKick();
|
|
466
488
|
}
|
|
467
489
|
|
|
@@ -502,8 +524,12 @@ async function settleTurn(ctx) {
|
|
|
502
524
|
try {
|
|
503
525
|
options.onSettled?.(detail);
|
|
504
526
|
} catch {}
|
|
527
|
+
// A quota failure suspends only a Goal that was running when it struck.
|
|
528
|
+
const quotaFailed = turn.failed && turn.quota && isCurrentTurn();
|
|
529
|
+
const goalBefore = quotaFailed ? runtime.goalStatus?.() || null : null;
|
|
530
|
+
let settledGoal = null;
|
|
505
531
|
try {
|
|
506
|
-
await bag.onGoalTurnSettled?.({
|
|
532
|
+
settledGoal = await bag.onGoalTurnSettled?.({
|
|
507
533
|
status: finalStatus,
|
|
508
534
|
error: turn.failureDiagnostic || turn.failureDetail || null,
|
|
509
535
|
usageLimited: turn.usageLimited,
|
|
@@ -520,6 +546,17 @@ async function settleTurn(ctx) {
|
|
|
520
546
|
}),
|
|
521
547
|
});
|
|
522
548
|
} catch {}
|
|
549
|
+
if (turn.failed && turn.quota) {
|
|
550
|
+
const suspended =
|
|
551
|
+
quotaFailed &&
|
|
552
|
+
!flags.disposed &&
|
|
553
|
+
goalBefore?.status === 'active' &&
|
|
554
|
+
settledGoal?.status === 'usage_limited' &&
|
|
555
|
+
settledGoal.id === goalBefore.id;
|
|
556
|
+
try {
|
|
557
|
+
bag.finishQuotaSettlement?.({ suspendedGoalId: suspended ? settledGoal.id : '' });
|
|
558
|
+
} catch {}
|
|
559
|
+
}
|
|
523
560
|
if (flags.goalSteeringAbortEpoch === turn.epoch) flags.goalSteeringAbortEpoch = null;
|
|
524
561
|
ctx.tuiDebug(
|
|
525
562
|
`runTurn end turn=${turn.index} status=${finalStatus} elapsedMs=${Date.now() - turn.startedAt} pending=${ctx.pending.length}`
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../src/gateway/route-meta.mjs';
|
|
18
18
|
import { getModelMetadataSync } from '../../../runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
19
19
|
import { resolveRuntimeRoot } from '../../../runtime/shared/runtime-root.mjs';
|
|
20
|
+
import { withDefaultPresets } from '../../../runtime/agent/orchestrator/config-presets.mjs';
|
|
20
21
|
|
|
21
22
|
function positiveInt(value) {
|
|
22
23
|
const n = parseInt(String(value || ''), 10);
|
|
@@ -142,7 +143,7 @@ function resolveConfiguredModelTarget(cfg, rawModel) {
|
|
|
142
143
|
const lower = stripped.toLowerCase();
|
|
143
144
|
const agent = cfg?.agent && typeof cfg.agent === 'object' ? cfg.agent : {};
|
|
144
145
|
const providers = agent.providers && typeof agent.providers === 'object' ? agent.providers : {};
|
|
145
|
-
const presets =
|
|
146
|
+
const presets = withDefaultPresets(agent.presets);
|
|
146
147
|
for (const p of presets) {
|
|
147
148
|
if (!p || typeof p !== 'object' || !p.provider || !p.model) continue;
|
|
148
149
|
const providerEntry = providers[p.provider];
|
|
@@ -455,7 +456,7 @@ function configuredGatewayStatus(options = {}) {
|
|
|
455
456
|
model = aliasTarget.model;
|
|
456
457
|
}
|
|
457
458
|
if (!provider || !model) return null;
|
|
458
|
-
const presets =
|
|
459
|
+
const presets = withDefaultPresets(cfg?.agent?.presets);
|
|
459
460
|
const preset = presets.find(p =>
|
|
460
461
|
p?.provider === provider &&
|
|
461
462
|
p?.model === model &&
|