mixdog 1.4.0 → 1.4.1
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/package.json +1 -1
- package/scripts/check-voice-runtime-inputs.mjs +25 -0
- package/scripts/gate-local.mjs +12 -3
- package/scripts/latest-guard.mjs +21 -9
- package/scripts/lib/isolated-test-env.mjs +10 -5
- package/scripts/lib/run-node-tests.mjs +37 -37
- package/scripts/lib/scoped-temp.d.mts +16 -0
- package/scripts/lib/scoped-temp.mjs +123 -0
- package/scripts/lib/stale-temp.mjs +44 -0
- package/scripts/post-release-check.mjs +1 -6
- package/scripts/pptx-kit-index.mjs +17 -4
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test-data-dir.mjs +10 -7
- package/src/app.mjs +4 -0
- package/src/defaults/shipped-history.json +5 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -4
- package/src/defaults/skills/computer-use/SKILL.md +4 -2
- package/src/lib/rules-builder.cjs +29 -12
- package/src/output-styles/common.md +2 -2
- package/src/output-styles/simple.md +1 -1
- package/src/rules/agent/AGENT.md +5 -4
- package/src/rules/lead/LEAD.md +10 -5
- package/src/rules/shared/10-tool-workflow.md +3 -1
- package/src/rules/shared/45-destructive.md +0 -2
- package/src/rules/shared/50-execution.md +2 -0
- package/src/rules/shared/70-delivery.md +7 -9
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
- package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
- package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
- package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
- package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
- package/src/runtime/channels/lib/whisper-port.mjs +10 -1
- package/src/runtime/computer-bridge/action-schema.mjs +2 -2
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
- package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
- package/src/runtime/local-provider/server-process.mjs +2 -1
- package/src/runtime/media/adapters/gemini-video.mjs +4 -1
- package/src/runtime/media/adapters/xai-media.mjs +2 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
- package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
- package/src/runtime/office/design/design-art-direction.mjs +29 -27
- package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
- package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
- package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
- package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
- package/src/runtime/office/quality/design-review.mjs +1 -5
- package/src/runtime/office/quality/document-brief.mjs +1 -5
- package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
- package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
- package/src/runtime/shared/bot-owned-work.mjs +56 -0
- package/src/runtime/shared/defaults-separation.mjs +1 -4
- package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
- package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
- package/src/runtime/shared/handoff-artifacts.mjs +18 -8
- package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
- package/src/runtime/shared/llm/cost.mjs +5 -1
- package/src/runtime/shared/loopback-listener.mjs +25 -2
- package/src/runtime/shared/model-route-chain.d.mts +7 -2
- package/src/runtime/shared/model-route-chain.mjs +22 -4
- package/src/runtime/shared/open-url.mjs +10 -0
- package/src/runtime/shared/session-activity-signal.mjs +14 -0
- package/src/runtime/shared/session-scratch.mjs +37 -0
- package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
- package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +3 -0
- package/src/runtime/shared/tool-work-units.mjs +2 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
- package/src/runtime/shared/worktrees/contract.mjs +4 -1
- package/src/runtime/shared/worktrees/git.mjs +15 -0
- package/src/runtime/shared/worktrees/manager.mjs +87 -38
- package/src/runtime/shared/worktrees/test-support.mjs +6 -10
- package/src/runtime/visualize-bridge/client.mjs +2 -0
- package/src/session-runtime/agent-home-delivery.mjs +41 -0
- package/src/session-runtime/agent-task-sessions.mjs +152 -63
- package/src/session-runtime/agent-tool-routing.mjs +8 -41
- package/src/session-runtime/boot/tools.mjs +5 -1
- package/src/session-runtime/bot-status.mjs +12 -6
- package/src/session-runtime/bot-target.mjs +38 -0
- package/src/session-runtime/channel-config-api.mjs +6 -1
- package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
- package/src/session-runtime/conversation-import/discovery.mjs +271 -25
- package/src/session-runtime/conversation-import/service.mjs +26 -5
- package/src/session-runtime/handoff-attachments.mjs +82 -31
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
- package/src/session-runtime/model-route/set-route.mjs +11 -2
- package/src/session-runtime/provider-catalog-cache.mjs +82 -36
- package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
- package/src/session-runtime/provider-models.mjs +4 -2
- package/src/session-runtime/resource-recall-api.mjs +6 -1
- package/src/session-runtime/runtime-feature-gates.mjs +5 -1
- package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
- package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
- package/src/session-runtime/services/agent-tool/render.mjs +71 -17
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
- package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
- package/src/session-runtime/services/provider-admin.mjs +2 -1
- package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
- package/src/session-runtime/workspace/transitions.mjs +25 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
- package/src/standalone/daemon-session-control.mjs +18 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
- package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
- package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
- package/src/tui/app/app-view/floating-panel.jsx +7 -4
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
- package/src/tui/app/model-picker/route-selection.mjs +5 -6
- package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
- package/src/tui/app/provider-setup/provider-items.mjs +8 -2
- package/src/tui/app/provider-setup-picker.mjs +9 -5
- package/src/tui/components/QuestionPanel.jsx +13 -4
- package/src/tui/dist/index.mjs +75 -28
- package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
- package/src/ui/statusline-agents.mjs +1 -3
|
@@ -453,8 +453,9 @@ function callArgs(call) {
|
|
|
453
453
|
return args && typeof args === 'object' ? args : {};
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
/** The paths one tool call wrote or produced
|
|
457
|
-
|
|
456
|
+
/** The paths one tool call wrote or produced; `written`: only those it
|
|
457
|
+
* actually created or modified (opening a file in visualize does neither). */
|
|
458
|
+
function toolCallOutputs(call, written) {
|
|
458
459
|
const name = String(call?.function?.name || call?.name || '');
|
|
459
460
|
const args = callArgs(call);
|
|
460
461
|
const text = (value) => (typeof value === 'string' && value.trim() ? [value.trim()] : []);
|
|
@@ -464,7 +465,7 @@ function toolCallOutputs(call) {
|
|
|
464
465
|
if (name === 'office') return OFFICE_WRITE_ACTIONS.has(args.action) ? [...text(args.output), ...text(args.path)] : [];
|
|
465
466
|
if (name === 'visualize') {
|
|
466
467
|
const input = args.input && typeof args.input === 'object' ? args.input : {};
|
|
467
|
-
const targets = args.action === 'open' && Array.isArray(input.targets) ? input.targets : [];
|
|
468
|
+
const targets = !written && args.action === 'open' && Array.isArray(input.targets) ? input.targets : [];
|
|
468
469
|
return [
|
|
469
470
|
...text(input.output),
|
|
470
471
|
...targets.filter((target) => typeof target === 'string' && !/^[a-z][a-z0-9+.-]*:\/\//i.test(target)),
|
|
@@ -520,8 +521,9 @@ export function turnSlice(messages, { runId = '', since = 0 } = {}) {
|
|
|
520
521
|
|
|
521
522
|
/** The candidate artifact paths of a turn's messages, most relevant first:
|
|
522
523
|
* what its final reply names, then media/office/visualize outputs, then the
|
|
523
|
-
* files it edited.
|
|
524
|
-
|
|
524
|
+
* files it edited. `written`: only the files it created or modified (no
|
|
525
|
+
* reply paths, no visualize open targets). */
|
|
526
|
+
export function turnArtifactCandidates(messages, { written = false } = {}) {
|
|
525
527
|
const list = Array.isArray(messages) ? messages : [];
|
|
526
528
|
const reply = list.findLast((message) => message?.role === 'assistant' && textOf(message.content).trim());
|
|
527
529
|
const produced = [];
|
|
@@ -535,10 +537,10 @@ export function turnArtifactCandidates(messages) {
|
|
|
535
537
|
: [];
|
|
536
538
|
for (const call of calls) {
|
|
537
539
|
const name = String(call?.function?.name || call?.name || '');
|
|
538
|
-
(name === 'edit' || name === 'apply_patch' ? edited : produced).push(...toolCallOutputs(call));
|
|
540
|
+
(name === 'edit' || name === 'apply_patch' ? edited : produced).push(...toolCallOutputs(call, written));
|
|
539
541
|
}
|
|
540
542
|
}
|
|
541
|
-
return [...(reply ? replyPaths(textOf(reply.content)) : []), ...produced, ...edited];
|
|
543
|
+
return [...(!written && reply ? replyPaths(textOf(reply.content)) : []), ...produced, ...edited];
|
|
542
544
|
}
|
|
543
545
|
|
|
544
546
|
/**
|
|
@@ -549,38 +551,16 @@ export function turnArtifactCandidates(messages) {
|
|
|
549
551
|
* the roots are left out silently; at most HANDOFF_LIMITS.count are kept.
|
|
550
552
|
*/
|
|
551
553
|
export function collectTurnArtifacts(messages, { cwd = '', roots = [] } = {}) {
|
|
552
|
-
const seen = new Set();
|
|
553
554
|
const files = [];
|
|
554
555
|
let omitted = 0;
|
|
555
556
|
const budget = createBudget();
|
|
556
|
-
for (const
|
|
557
|
-
// A candidate is a lexical path; its canonical form dedupes after the read.
|
|
558
|
-
const lexical = samePlatformPath(
|
|
559
|
-
isAbsolute(candidate) ? resolve(candidate) : resolve(cwd || process.cwd(), candidate)
|
|
560
|
-
);
|
|
561
|
-
if (seen.has(lexical)) continue;
|
|
562
|
-
seen.add(lexical);
|
|
563
|
-
let file;
|
|
564
|
-
try {
|
|
565
|
-
file = readContained(candidate, { cwd, roots, surface: 'artifact' });
|
|
566
|
-
} catch {
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
const key = samePlatformPath(file.path);
|
|
570
|
-
if (key !== lexical && seen.has(key)) continue;
|
|
571
|
-
seen.add(key);
|
|
557
|
+
for (const { file } of readableCandidates(turnArtifactCandidates(messages), { cwd, roots })) {
|
|
572
558
|
if (files.length >= HANDOFF_LIMITS.count) {
|
|
573
559
|
omitted += 1;
|
|
574
560
|
continue;
|
|
575
561
|
}
|
|
576
562
|
const { type, bytes } = file;
|
|
577
|
-
const entry =
|
|
578
|
-
path: file.path,
|
|
579
|
-
name: file.name,
|
|
580
|
-
kind: type?.kind || 'file',
|
|
581
|
-
mimeType: type?.mimeType || 'application/octet-stream',
|
|
582
|
-
size: file.size,
|
|
583
|
-
};
|
|
563
|
+
const entry = pathEntry(file);
|
|
584
564
|
const storable = bytes && (type.kind !== 'image' || SUPPORTED_IMAGE_MIME_TYPES.has(type.mimeType));
|
|
585
565
|
if (storable && budget.take(type.kind, bytes.length)) {
|
|
586
566
|
try {
|
|
@@ -591,3 +571,74 @@ export function collectTurnArtifacts(messages, { cwd = '', roots = [] } = {}) {
|
|
|
591
571
|
}
|
|
592
572
|
return { files, omitted };
|
|
593
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The files `messages` (one turn) created or modified, inside `roots`, that
|
|
577
|
+
* `report` (the text handed to the Lead) does not name: `{ files, omitted }`
|
|
578
|
+
* as path-only entries, which the Lead forwards by path. Nothing is stored.
|
|
579
|
+
*/
|
|
580
|
+
export function collectUnreportedWrites(messages, { cwd = '', roots = [], report }) {
|
|
581
|
+
const found = [...readableCandidates(turnArtifactCandidates(messages, { written: true }), { cwd, roots })];
|
|
582
|
+
const unnamed = found.filter((item) => !reportNames(report, item, found, cwd));
|
|
583
|
+
return {
|
|
584
|
+
files: unnamed.slice(0, HANDOFF_LIMITS.count).map(({ file }) => pathEntry(file)),
|
|
585
|
+
omitted: Math.max(0, unnamed.length - HANDOFF_LIMITS.count),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const lexicalKey = (path, cwd) => samePlatformPath(resolve(cwd || process.cwd(), path));
|
|
590
|
+
|
|
591
|
+
const pathEntry = (file) => ({
|
|
592
|
+
path: file.path,
|
|
593
|
+
name: file.name,
|
|
594
|
+
kind: file.type?.kind || 'file',
|
|
595
|
+
mimeType: file.type?.mimeType || 'application/octet-stream',
|
|
596
|
+
size: file.size,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
/** The distinct files among `candidates` that can be read inside `roots`, with
|
|
600
|
+
* their lexical and canonical keys. A candidate is a lexical path; its
|
|
601
|
+
* canonical form dedupes after the read. */
|
|
602
|
+
function* readableCandidates(candidates, { cwd, roots }) {
|
|
603
|
+
const seen = new Set();
|
|
604
|
+
for (const candidate of candidates) {
|
|
605
|
+
const lexical = lexicalKey(candidate, cwd);
|
|
606
|
+
if (seen.has(lexical)) continue;
|
|
607
|
+
seen.add(lexical);
|
|
608
|
+
let file;
|
|
609
|
+
try {
|
|
610
|
+
file = readContained(candidate, { cwd, roots, surface: 'artifact' });
|
|
611
|
+
} catch {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const key = samePlatformPath(file.path);
|
|
615
|
+
if (key !== lexical && seen.has(key)) continue;
|
|
616
|
+
seen.add(key);
|
|
617
|
+
yield { file, lexical, key };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Whether `report` names the produced file `item` among `found`: by its full
|
|
622
|
+
* path or a cwd-relative path resolving to it exactly, or by its bare file name
|
|
623
|
+
* when no other produced file shares that name. */
|
|
624
|
+
function reportNames(report, { file, lexical, key }, found, cwd) {
|
|
625
|
+
const flat = (value) => String(value).replaceAll('\\', '/');
|
|
626
|
+
const filePath = flat(file.path);
|
|
627
|
+
if (flat(report).includes(filePath)) return true;
|
|
628
|
+
const plain = report
|
|
629
|
+
.split(/[\s`"'<>()[\],;|]+/)
|
|
630
|
+
.map((token) => token.replace(/:\d+(?::\d+)?$/, '').replace(/[.:!?]+$/, ''))
|
|
631
|
+
.filter((token) => /\.[A-Za-z0-9]{1,8}$/.test(token) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(token));
|
|
632
|
+
const refs = new Set([...replyPaths(report), ...plain]);
|
|
633
|
+
const unique = found.filter((other) => other.file.name === file.name).length === 1;
|
|
634
|
+
for (const ref of refs) {
|
|
635
|
+
const normalized = flat(ref).replace(/^(?:\.\/)+/, '');
|
|
636
|
+
if (!normalized.includes('/')) {
|
|
637
|
+
if (unique && normalized === file.name) return true;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const refKey = lexicalKey(ref, cwd);
|
|
641
|
+
if (refKey === lexical || refKey === key) return true;
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// ask_user: put 1-4 multiple-choice questions to the user through the session's
|
|
2
2
|
// tool-approval hook (kind: 'question') and report the answers to the model.
|
|
3
|
+
// handoff_user (the secret / takeover handoff) shares this module's human-gate code.
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
+
validateUserHandoff,
|
|
6
7
|
validateAskUserQuestions,
|
|
7
8
|
ASK_USER_MAX_TEXT,
|
|
8
9
|
} from '../../runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs';
|
|
@@ -78,7 +79,7 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
|
|
|
78
79
|
const gate = {
|
|
79
80
|
agentId: agentIdForSession(sessionId),
|
|
80
81
|
kind: handoff.kind,
|
|
81
|
-
tool: `
|
|
82
|
+
tool: `handoff_user:${handoff.surface}`,
|
|
82
83
|
target: handoff.target || handoff.field || '',
|
|
83
84
|
};
|
|
84
85
|
// A schedule or webhook run has nobody to take over: the step ends it now,
|
|
@@ -133,7 +134,7 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
|
|
|
133
134
|
}
|
|
134
135
|
const result = await runAbortable(signal, () =>
|
|
135
136
|
hook({
|
|
136
|
-
name: '
|
|
137
|
+
name: 'handoff_user',
|
|
137
138
|
kind: 'secret',
|
|
138
139
|
secret: { requestId, surface: handoff.surface, target: handoff.target, field: handoff.field, ...bound },
|
|
139
140
|
reason: '',
|
|
@@ -171,7 +172,7 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
|
|
|
171
172
|
: null;
|
|
172
173
|
const result = await runAbortable(signal, () =>
|
|
173
174
|
hook({
|
|
174
|
-
name: '
|
|
175
|
+
name: 'handoff_user',
|
|
175
176
|
kind: 'takeover',
|
|
176
177
|
takeover: {
|
|
177
178
|
surface: handoff.surface,
|
|
@@ -278,16 +279,14 @@ function formatAskUserResult(questions, answers, reply) {
|
|
|
278
279
|
export function createAskUserToolHandlers({ rt }) {
|
|
279
280
|
const { sessionIdFor, signalFor } = createCallerContextResolvers(rt);
|
|
280
281
|
return {
|
|
282
|
+
handoff_user: async (args, { callerCtx }) =>
|
|
283
|
+
runAskUserHandoff(validateUserHandoff(args), {
|
|
284
|
+
hook: callerCtx?.toolApprovalHook,
|
|
285
|
+
sessionId: sessionIdFor(callerCtx),
|
|
286
|
+
toolCallId: callerCtx?.toolCallId || null,
|
|
287
|
+
signal: signalFor(callerCtx),
|
|
288
|
+
}),
|
|
281
289
|
ask_user: async (args, { callerCtx }) => {
|
|
282
|
-
const handoff = validateAskUserHandoff(args);
|
|
283
|
-
if (handoff) {
|
|
284
|
-
return runAskUserHandoff(handoff, {
|
|
285
|
-
hook: callerCtx?.toolApprovalHook,
|
|
286
|
-
sessionId: sessionIdFor(callerCtx),
|
|
287
|
-
toolCallId: callerCtx?.toolCallId || null,
|
|
288
|
-
signal: signalFor(callerCtx),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
290
|
const questions = validateAskUserQuestions(args);
|
|
292
291
|
const hook = callerCtx?.toolApprovalHook;
|
|
293
292
|
if (typeof hook !== 'function') return NO_USER;
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
// the message is the user's row in the room's log. Never a coding session, a
|
|
5
5
|
// delegated worker, or a run nobody attends.
|
|
6
6
|
import { isAgentOwner } from '../../runtime/agent/orchestrator/agent-owner.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
noteReactionTarget,
|
|
9
|
+
validateReactionArgs,
|
|
10
|
+
} from '../../runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs';
|
|
8
11
|
import { agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
|
|
9
12
|
import { roomMemberOfSession, roomSessionId } from '../../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
10
13
|
import { reactInSession } from '../../runtime/agent/orchestrator/session/message-reactions.mjs';
|
|
@@ -17,6 +17,7 @@ import { scheduleCallerScope } from '../services/schedule-access.mjs';
|
|
|
17
17
|
import { worktreeCallerScope } from '../services/worktree-access.mjs';
|
|
18
18
|
import { resolveHandoffAttachments, sessionRoots } from '../handoff-attachments.mjs';
|
|
19
19
|
import { delegationChainOf } from '../bot-chain.mjs';
|
|
20
|
+
import { BOT_TARGET_ACTIONS, resolveBotTarget } from '../bot-target.mjs';
|
|
20
21
|
|
|
21
22
|
const USER_TITLE_ERROR = 'session: the user already titled this session; the title was not changed';
|
|
22
23
|
|
|
@@ -50,6 +51,7 @@ export function createWorkspaceToolHandlers({
|
|
|
50
51
|
getSessionManager = () => null,
|
|
51
52
|
getLifecycleApi = () => null,
|
|
52
53
|
executeSessionControl = null,
|
|
54
|
+
listBots,
|
|
53
55
|
}) {
|
|
54
56
|
const assertLead = (action, callerCtx) => {
|
|
55
57
|
const callerSessionId = callerCtx?.callerSessionId || rt.session?.id || null;
|
|
@@ -110,12 +112,16 @@ export function createWorkspaceToolHandlers({
|
|
|
110
112
|
'session: create belongs to a bot conversation; delegated workers, agent task sessions and schedule/webhook runs cannot create agent task sessions'
|
|
111
113
|
);
|
|
112
114
|
}
|
|
115
|
+
// A bot id names that bot's home chat; every check below and in the
|
|
116
|
+
// control runs on the chat it resolves to.
|
|
117
|
+
const sessionTarget =
|
|
118
|
+
BOT_TARGET_ACTIONS.has(action) && target ? await resolveBotTarget(target, listBots) : target;
|
|
113
119
|
// A task session reaches its agent only through its reported result.
|
|
114
120
|
const origin = rt.session?.taskOrigin;
|
|
115
121
|
if (
|
|
116
122
|
action === 'send' &&
|
|
117
123
|
origin &&
|
|
118
|
-
(
|
|
124
|
+
(sessionTarget === origin.sessionId || agentIdOfHomeSession(sessionTarget) === origin.agentId)
|
|
119
125
|
) {
|
|
120
126
|
throw new Error('session: a task session reports to its agent only through its result');
|
|
121
127
|
}
|
|
@@ -129,7 +135,7 @@ export function createWorkspaceToolHandlers({
|
|
|
129
135
|
const attachmentParts = callerAttachmentParts(rt, args, callerCwd, 'session');
|
|
130
136
|
return JSON.stringify(
|
|
131
137
|
await executeSessionControl(
|
|
132
|
-
{ ...rest, action },
|
|
138
|
+
{ ...rest, action, ...(sessionTarget !== target ? { session_id: sessionTarget } : {}) },
|
|
133
139
|
{
|
|
134
140
|
callerSessionId,
|
|
135
141
|
callerAgentId,
|
|
@@ -150,14 +150,19 @@ export function buildColdHeir(source, now = Date.now()) {
|
|
|
150
150
|
// earlier folder) is the source's own: the heir's prompt is built for the
|
|
151
151
|
// folder it actually starts in. Project memory follows the Project, which a
|
|
152
152
|
// worktree shares, so the source's current memory slice carries over.
|
|
153
|
+
// In the same folder only the environment facts are rebuilt: they name the
|
|
154
|
+
// heir's own scratch directory, never the source's, while the inherited
|
|
155
|
+
// instruction files and memory stay as the source presented them.
|
|
153
156
|
const pending = source.workspaceReroot;
|
|
154
157
|
delete heir.workspaceReroot;
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
if (heir.cwd) {
|
|
159
|
+
const context = buildWorkspacePromptContext(heir, heir.cwd);
|
|
160
|
+
refreshSessionWorkspaceContext(
|
|
161
|
+
heir,
|
|
162
|
+
projectFolder || pending
|
|
163
|
+
? { cwd: heir.cwd, ...context, coreMemoryContext: pending?.target?.coreMemoryContext }
|
|
164
|
+
: { cwd: heir.cwd, environmentContext: context.environmentContext }
|
|
165
|
+
);
|
|
161
166
|
}
|
|
162
167
|
invalidateContextUsageSnapshot(heir);
|
|
163
168
|
invalidateProviderContextBaseline(heir);
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
wantsIncomingTail,
|
|
22
22
|
} from '../../runtime/agent/orchestrator/session/store-summary-last-message.mjs';
|
|
23
23
|
import { isRealTurnMessage } from '../../runtime/agent/orchestrator/session/compact/user-message-kind.mjs';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
compactCount as compactCountOf,
|
|
26
|
+
compactCountField,
|
|
27
|
+
realTurnCount as realTurnCountOf,
|
|
28
|
+
} from '../../runtime/agent/orchestrator/session/store-summary-fields.mjs';
|
|
25
29
|
|
|
26
30
|
const VISIBLE_OWNERS = ['cli', 'user', 'mixdog'];
|
|
27
31
|
|
|
@@ -107,6 +111,10 @@ export function leadSessionRow(s, working = false) {
|
|
|
107
111
|
// sit among the counted ones: a bot row's unread pill counts those past
|
|
108
112
|
// the read cursor. Absent when unknown; clients then show one.
|
|
109
113
|
...(incomingTail ? { incomingTail } : {}),
|
|
114
|
+
// The numbering epoch of messageCount: each compaction restarts it. A
|
|
115
|
+
// summary row carries it at the top; a stored session in its compaction
|
|
116
|
+
// record. Unknown stays absent.
|
|
117
|
+
...compactCountField(compactCountOf(s) === null ? s.compaction : s),
|
|
110
118
|
title: cleanSessionPreview(s.title || '', 100),
|
|
111
119
|
preview,
|
|
112
120
|
// The latest user or assistant line (an agent contact's row shows it).
|
|
@@ -217,11 +217,20 @@ function switchSessionInPlace(deps, session) {
|
|
|
217
217
|
|
|
218
218
|
/** The Lead's fallback models after this selection, checked like an agent's
|
|
219
219
|
* chain (leaf routes, distinct providers): an explicit list replaces the
|
|
220
|
-
* stored one ([] clears it), a selection without one keeps it
|
|
220
|
+
* stored one ([] clears it), a selection without one keeps it as stored — a
|
|
221
|
+
* fallback on the new main model's provider stays saved and is inactive
|
|
222
|
+
* (executionFallbacks). */
|
|
221
223
|
async function leadFallbackChain(deps, next, primary) {
|
|
222
224
|
const storedPreset = findPreset(deps.getConfig(), workflowPresetId('lead'));
|
|
223
|
-
const
|
|
225
|
+
const explicit = hasOwn(next || {}, 'fallbacks');
|
|
226
|
+
const requested = explicit ? next.fallbacks : storedPreset?.fallbacks;
|
|
224
227
|
if (!Array.isArray(requested) || !requested.length) return [];
|
|
228
|
+
if (!explicit) {
|
|
229
|
+
return (
|
|
230
|
+
normalizeWorkflowRoute({ provider: primary.provider, model: primary.model, fallbacks: requested })?.fallbacks ||
|
|
231
|
+
[]
|
|
232
|
+
);
|
|
233
|
+
}
|
|
225
234
|
const chain = routeChain({ provider: primary.provider, model: primary.model, fallbacks: requested });
|
|
226
235
|
validateRouteChainProviders(chain);
|
|
227
236
|
const storedChain = routeChain(storedPreset || {});
|
|
@@ -37,6 +37,19 @@ let sharedPartialEntries = null;
|
|
|
37
37
|
let sharedCatalogPromise = null;
|
|
38
38
|
let sharedCatalogPromiseKey = null;
|
|
39
39
|
let sharedCatalogPromiseForced = false;
|
|
40
|
+
// Every read that publishes to the shared state (a full load or a scoped
|
|
41
|
+
// refresh) runs through this queue, one at a time, so the last write is always
|
|
42
|
+
// the newest successful answer.
|
|
43
|
+
// A catalog state that moved on (new revision or account epoch) starts a fresh
|
|
44
|
+
// queue: reads of the old state never publish, so nothing waits for them.
|
|
45
|
+
let publishing = { key: null, tail: Promise.resolve() };
|
|
46
|
+
function serial(key, run) {
|
|
47
|
+
if (publishing.key !== key) publishing = { key, tail: Promise.resolve() };
|
|
48
|
+
const queue = publishing;
|
|
49
|
+
const result = queue.tail.then(run);
|
|
50
|
+
queue.tail = result.catch(() => {});
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
export function catalogRevision(registry) {
|
|
42
55
|
const value = Number(registry?.providerCatalogRevision?.());
|
|
@@ -60,6 +73,24 @@ export function invalidateSharedProviderCatalog() {
|
|
|
60
73
|
|
|
61
74
|
const answered = (entry) => !Object.hasOwn(entry, 'error');
|
|
62
75
|
|
|
76
|
+
/** Folds a scoped refresh into the shared state of `key`: only the providers it
|
|
77
|
+
* answered are replaced; a failed entry replaces nothing. */
|
|
78
|
+
function mergeScopedEntries(key, scoped) {
|
|
79
|
+
const fresh = new Map(scoped.filter(answered).map((entry) => [entry.name, entry]));
|
|
80
|
+
const merge = (current) => current.map((entry) => fresh.get(entry.name) ?? entry);
|
|
81
|
+
if (sharedCatalogKey === key) {
|
|
82
|
+
sharedCatalogEntries = merge(sharedCatalogEntries);
|
|
83
|
+
} else if (sharedPartialKey === key) {
|
|
84
|
+
const merged = merge(sharedPartialEntries);
|
|
85
|
+
if (merged.every(answered)) {
|
|
86
|
+
// The refresh answered the last missing provider: the catalog is whole.
|
|
87
|
+
[sharedCatalogKey, sharedCatalogEntries, sharedPartialKey, sharedPartialEntries] = [key, merged, null, null];
|
|
88
|
+
} else {
|
|
89
|
+
sharedPartialEntries = merged;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
async function providerEntry(name, provider, timeoutMs, epoch) {
|
|
64
95
|
if (typeof provider?.listModels !== 'function') return { name, models: [], ms: 0 };
|
|
65
96
|
const startedAt = performance.now();
|
|
@@ -78,6 +109,23 @@ async function providerEntry(name, provider, timeoutMs, epoch) {
|
|
|
78
109
|
return { name, models: Array.isArray(outcome.value) ? outcome.value : [], ms };
|
|
79
110
|
}
|
|
80
111
|
|
|
112
|
+
/** Fresh entries for just the named providers (an account-pool provider is one
|
|
113
|
+
* entry, however many accounts it holds), merged into the shared catalog. */
|
|
114
|
+
export function scopedProviderCatalog(registry, names, { timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS } = {}) {
|
|
115
|
+
const key = catalogKey(registry);
|
|
116
|
+
const wanted = new Set(names);
|
|
117
|
+
return serial(key, async () => {
|
|
118
|
+
const entries = await Promise.all(
|
|
119
|
+
[...registry.getAllProviders()]
|
|
120
|
+
.filter(([name]) => wanted.has(name))
|
|
121
|
+
.map(([name, provider]) => providerEntry(name, provider, timeoutMs, key))
|
|
122
|
+
);
|
|
123
|
+
// A catalog state that moved on while this read ran is not written to.
|
|
124
|
+
if (catalogKey(registry) === key) mergeScopedEntries(key, entries);
|
|
125
|
+
return entries;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
81
129
|
export async function sharedProviderCatalog(registry, { force = false, timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS } = {}) {
|
|
82
130
|
const key = catalogKey(registry);
|
|
83
131
|
if (!force && sharedCatalogKey === key && Array.isArray(sharedCatalogEntries)) {
|
|
@@ -86,45 +134,43 @@ export async function sharedProviderCatalog(registry, { force = false, timeoutMs
|
|
|
86
134
|
if (sharedCatalogPromise && sharedCatalogPromiseKey === key && (sharedCatalogPromiseForced || !force)) {
|
|
87
135
|
return await sharedCatalogPromise;
|
|
88
136
|
}
|
|
89
|
-
const kept = new Map();
|
|
90
|
-
if (!force && sharedPartialKey === key) {
|
|
91
|
-
const now = Date.now();
|
|
92
|
-
for (const entry of sharedPartialEntries) {
|
|
93
|
-
if (answered(entry) || now < entry.retryAt) kept.set(entry.name, entry);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const providers = [...registry.getAllProviders()];
|
|
97
|
-
// Every missing provider is still inside its backoff: the partial answer is
|
|
98
|
-
// the answer, without asking anyone.
|
|
99
|
-
if (providers.length > 0 && providers.every(([name]) => kept.has(name))) {
|
|
100
|
-
return providers.map(([name]) => kept.get(name));
|
|
101
|
-
}
|
|
102
|
-
// A pending list from an older catalog state (revision or account epoch) is
|
|
103
|
-
// not joined: its answer may describe the previous catalog.
|
|
104
|
-
const epoch = key;
|
|
105
137
|
sharedCatalogPromiseKey = key;
|
|
106
138
|
sharedCatalogPromiseForced = force;
|
|
107
|
-
const request =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const partial = !complete && entries.some(answered);
|
|
114
|
-
sharedCatalogKey = complete ? key : null;
|
|
115
|
-
sharedCatalogEntries = complete ? entries : null;
|
|
116
|
-
sharedPartialKey = partial ? key : null;
|
|
117
|
-
sharedPartialEntries = partial ? entries : null;
|
|
139
|
+
const request = serial(key, async () => {
|
|
140
|
+
const kept = new Map();
|
|
141
|
+
if (!force && sharedPartialKey === key) {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
for (const entry of sharedPartialEntries) {
|
|
144
|
+
if (answered(entry) || now < entry.retryAt) kept.set(entry.name, entry);
|
|
118
145
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
}
|
|
147
|
+
const providers = [...registry.getAllProviders()];
|
|
148
|
+
// Every missing provider is still inside its backoff: the partial answer is
|
|
149
|
+
// the answer, without asking anyone.
|
|
150
|
+
if (providers.length > 0 && providers.every(([name]) => kept.has(name))) {
|
|
151
|
+
return providers.map(([name]) => kept.get(name));
|
|
152
|
+
}
|
|
153
|
+
// A pending list from an older catalog state (revision or account epoch) is
|
|
154
|
+
// not joined: its answer may describe the previous catalog.
|
|
155
|
+
const entries = await Promise.all(
|
|
156
|
+
providers.map(([name, provider]) => kept.get(name) || providerEntry(name, provider, timeoutMs, key))
|
|
157
|
+
);
|
|
158
|
+
if (sharedCatalogPromise === request && catalogKey(registry) === key) {
|
|
159
|
+
const complete = entries.every(answered);
|
|
160
|
+
const partial = !complete && entries.some(answered);
|
|
161
|
+
sharedCatalogKey = complete ? key : null;
|
|
162
|
+
sharedCatalogEntries = complete ? entries : null;
|
|
163
|
+
sharedPartialKey = partial ? key : null;
|
|
164
|
+
sharedPartialEntries = partial ? entries : null;
|
|
165
|
+
}
|
|
166
|
+
return entries;
|
|
167
|
+
}).finally(() => {
|
|
168
|
+
if (sharedCatalogPromise === request) {
|
|
169
|
+
sharedCatalogPromise = null;
|
|
170
|
+
sharedCatalogPromiseKey = null;
|
|
171
|
+
sharedCatalogPromiseForced = false;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
128
174
|
sharedCatalogPromise = request;
|
|
129
175
|
return await request;
|
|
130
176
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// catalog flattened into deduplicated rows with per-provider profiling.
|
|
5
5
|
import { modelProxyClientMode } from '../../runtime/shared/model-proxy-mode.mjs';
|
|
6
6
|
import { isSelectableLlmModel } from '../model-recency.mjs';
|
|
7
|
-
import { sharedProviderCatalog } from '../provider-catalog-cache.mjs';
|
|
7
|
+
import { scopedProviderCatalog, sharedProviderCatalog } from '../provider-catalog-cache.mjs';
|
|
8
|
+
import { providerRows } from '../services/agent-tool/model-select.mjs';
|
|
8
9
|
import { NO_ENABLED_PROVIDERS } from '../../runtime/agent/orchestrator/providers/registry-errors.mjs';
|
|
9
10
|
|
|
10
11
|
export function createCatalogLoader({
|
|
@@ -42,7 +43,7 @@ export function createCatalogLoader({
|
|
|
42
43
|
* refresh invalidates it, leaving the UI on the stale first-open rows.
|
|
43
44
|
* Resolves false when no provider is enabled: a fresh install has nothing
|
|
44
45
|
* to load, and the registry refuses an all-disabled init by design. */
|
|
45
|
-
async function prepare({ forceRefresh, loadSecrets }) {
|
|
46
|
+
async function prepare({ forceRefresh, loadSecrets, provider }) {
|
|
46
47
|
if (loadSecrets) {
|
|
47
48
|
const secretsStartedAt = performance.now();
|
|
48
49
|
await awaitKeychainPrewarm();
|
|
@@ -69,7 +70,15 @@ export function createCatalogLoader({
|
|
|
69
70
|
if (!forceRefresh && typeof reg().refreshProviderCatalogsOnStartup === 'function') {
|
|
70
71
|
await reg().refreshProviderCatalogsOnStartup();
|
|
71
72
|
}
|
|
72
|
-
if (forceRefresh &&
|
|
73
|
+
if (forceRefresh && provider) {
|
|
74
|
+
// A provider-scoped refresh asks only the registry providers the name
|
|
75
|
+
// matches and folds their entries into the shared catalog; every other
|
|
76
|
+
// provider keeps what it loaded.
|
|
77
|
+
const known = [...reg().getAllProviders().keys()].map((name) => ({ provider: name }));
|
|
78
|
+
const names = providerRows(known, provider).map((row) => row.provider);
|
|
79
|
+
await reg().refreshProviderCatalogsFor(names);
|
|
80
|
+
await scopedProviderCatalog(reg(), names, { timeoutMs: providerTimeoutMs });
|
|
81
|
+
} else if (forceRefresh && typeof reg().refreshCatalogs === 'function') {
|
|
73
82
|
await reg().refreshCatalogs({ force: true });
|
|
74
83
|
}
|
|
75
84
|
profile('catalog-refresh-ready', { ms: (performance.now() - refreshStartedAt).toFixed(1) });
|
|
@@ -81,13 +90,18 @@ export function createCatalogLoader({
|
|
|
81
90
|
* providers that failed or timed out, `request.partial` marks a load
|
|
82
91
|
* where some but not all providers answered, and `request.retryAt` is when
|
|
83
92
|
* the first missing provider may be asked again. */
|
|
84
|
-
return async function loadProviderModelsFresh({
|
|
93
|
+
return async function loadProviderModelsFresh({
|
|
94
|
+
forceRefresh = false,
|
|
95
|
+
loadSecrets = true,
|
|
96
|
+
request = null,
|
|
97
|
+
provider = null,
|
|
98
|
+
} = {}) {
|
|
85
99
|
const startedAt = performance.now();
|
|
86
100
|
profile('load:start', { forceRefresh, loadSecrets });
|
|
87
101
|
// No enabled provider is an empty catalog, not a failure. The request
|
|
88
102
|
// stays incomplete so this answer is never cached: connecting a provider
|
|
89
103
|
// is picked up by the next read.
|
|
90
|
-
if (!(await prepare({ forceRefresh, loadSecrets }))) {
|
|
104
|
+
if (!(await prepare({ forceRefresh, loadSecrets, provider }))) {
|
|
91
105
|
profile('load:done', { ms: (performance.now() - startedAt).toFixed(1), providers: 0, rows: 0 });
|
|
92
106
|
return [];
|
|
93
107
|
}
|
|
@@ -97,7 +111,10 @@ export function createCatalogLoader({
|
|
|
97
111
|
// forward only for the still-current request, never for a superseded load.
|
|
98
112
|
if (ownsLoad) request.seq = caches.providerModelsLoadSeq;
|
|
99
113
|
const seq = request?.seq ?? caches.providerModelsLoadSeq;
|
|
100
|
-
const catalogEntries = await sharedProviderCatalog(reg(), {
|
|
114
|
+
const catalogEntries = await sharedProviderCatalog(reg(), {
|
|
115
|
+
force: forceRefresh && !provider,
|
|
116
|
+
timeoutMs: providerTimeoutMs,
|
|
117
|
+
});
|
|
101
118
|
if (request) {
|
|
102
119
|
const failed = catalogEntries.filter((entry) => Object.hasOwn(entry, 'error'));
|
|
103
120
|
request.missing = failed.map((entry) => entry.name);
|
|
@@ -156,7 +156,9 @@ export function createProviderModels({
|
|
|
156
156
|
return promise;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
/** `provider` (a name or fragment of one) scopes a forced read: only the
|
|
160
|
+
* providers it names are refreshed, the rest keep their loaded entries. */
|
|
161
|
+
async function collectProviderModels({ force = false, quick = false, provider = null } = {}) {
|
|
160
162
|
meta.syncCatalogRevision();
|
|
161
163
|
const cached = caches.providerModelsCache;
|
|
162
164
|
if (!force && Array.isArray(cached.models)) {
|
|
@@ -180,7 +182,7 @@ export function createProviderModels({
|
|
|
180
182
|
}
|
|
181
183
|
if (force) {
|
|
182
184
|
const request = { seq: ++caches.providerModelsLoadSeq };
|
|
183
|
-
const models = await loadProviderModelsFresh({ forceRefresh: true, loadSecrets: true, request });
|
|
185
|
+
const models = await loadProviderModelsFresh({ forceRefresh: true, loadSecrets: true, request, provider });
|
|
184
186
|
adoptProviderModelCache(models, request);
|
|
185
187
|
return withCatalogState(rows.providerModelsFromCacheRows(models), partialOf(request));
|
|
186
188
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Memory control and recall: the current session's own rows first, then the
|
|
2
2
|
// memory runtime (with a forced ingest of the live session when it is empty).
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
clean,
|
|
5
|
+
toolResponseText,
|
|
6
|
+
isEmptyRecallText,
|
|
7
|
+
currentSessionRecallRows,
|
|
8
|
+
} from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
|
|
4
9
|
|
|
5
10
|
function isIdentityRecallQuery(query) {
|
|
6
11
|
const q = clean(query).toLowerCase().replace(/\s+/g, '');
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Key names mirror the dependency names of settings-api.mjs / the session-build
|
|
6
6
|
// deps, so the runtime can hand them straight through.
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
builtinFeatureActive,
|
|
9
|
+
featureDisallowedToolsFor,
|
|
10
|
+
localGitToolsActive,
|
|
11
|
+
} from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
|
|
8
12
|
import { recapEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
|
|
9
13
|
import { browserBridgeAvailableSync } from '../runtime/browser-bridge/client.mjs';
|
|
10
14
|
import { computerBridgeAvailableSync } from '../runtime/computer-bridge/client.mjs';
|