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
|
@@ -366,7 +366,9 @@ let deps = DEFAULT_DEPS;
|
|
|
366
366
|
|
|
367
367
|
// The idle auto-clear measures idleness from the last cache use, a
|
|
368
368
|
// successful warm included (durable in the touch store).
|
|
369
|
-
|
|
369
|
+
// The record names the route and account the warm read, so only a request
|
|
370
|
+
// on that same prefix counts it as fresh cache (compact-policy.mjs).
|
|
371
|
+
setCacheWarmUseSource((sessionId) => (sessionId ? deps.touches().warmUse(sessionId) : null));
|
|
370
372
|
|
|
371
373
|
// A session deleted for good drops everything it learned and touched.
|
|
372
374
|
registerSessionPurgeHook((sessionId) => {
|
|
@@ -787,6 +789,10 @@ async function fireWarm(sessionId, chain) {
|
|
|
787
789
|
chain.latencyMs = (reportedAt ?? deps.now()) - sentAt;
|
|
788
790
|
recordTouch(sessionId, chain);
|
|
789
791
|
// A read without a write is cache use: the idle auto-clear counts from it.
|
|
790
|
-
deps.touches().recordWarmUse(sessionId, sentAt
|
|
792
|
+
deps.touches().recordWarmUse(sessionId, sentAt, {
|
|
793
|
+
provider: chain.request.provider.name,
|
|
794
|
+
model: chain.request.model,
|
|
795
|
+
accountId: target.accountId,
|
|
796
|
+
});
|
|
791
797
|
scheduleWarm(sessionId, chain);
|
|
792
798
|
}
|
|
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto';
|
|
|
11
11
|
|
|
12
12
|
/** Bumped whenever the table layout or a codec changes: the DB is then
|
|
13
13
|
* dropped and backfilled again from the JSON catalog. */
|
|
14
|
-
export const SCHEMA_VERSION =
|
|
14
|
+
export const SCHEMA_VERSION = 11;
|
|
15
15
|
/** PRAGMA application_id ('MXSC'): a file without it is not this catalog. */
|
|
16
16
|
export const APPLICATION_ID = 0x4d585343;
|
|
17
17
|
|
|
@@ -68,6 +68,9 @@ const FIELDS = [
|
|
|
68
68
|
// Places of the incoming messages with text among the counted ones, for the
|
|
69
69
|
// unread count of a bot home / room log; absent otherwise (schema 10).
|
|
70
70
|
['incomingTail', 'incoming_tail', 'optjson'],
|
|
71
|
+
// Compactions the session has had: the epoch of its message numbering;
|
|
72
|
+
// absent for a row written before schema 11.
|
|
73
|
+
['compactCount', 'compact_count', 'optjson'],
|
|
71
74
|
];
|
|
72
75
|
|
|
73
76
|
const COLUMN_TYPES = { text: 'TEXT', real: 'REAL', int: 'INTEGER', bool: 'INTEGER', json: 'TEXT', optjson: 'TEXT' };
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isProtectedContextAckMessage,
|
|
10
10
|
latestActualUserInstructionIndex,
|
|
11
11
|
pendingTaskNotificationIndexes,
|
|
12
|
+
requestStart,
|
|
12
13
|
} from './messages.mjs';
|
|
13
14
|
import { staleObservations, supersedeObservation } from './observation-supersession.mjs';
|
|
14
15
|
import { carryMessageId, withoutMessageIdsReplacer } from '../message-ids.mjs';
|
|
@@ -101,14 +102,6 @@ export function executionTokens(messages) {
|
|
|
101
102
|
);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
function requestStart(messages, index) {
|
|
105
|
-
for (let i = index; i >= 0; i -= 1) {
|
|
106
|
-
if (!isActualUserInstructionMessage(messages[i])) continue;
|
|
107
|
-
if (messages[i]?.meta?.source !== 'steering') return i;
|
|
108
|
-
}
|
|
109
|
-
return latestActualUserInstructionIndex(messages.slice(0, index + 1));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
105
|
// Each assistant tool-call turn with the tool results that directly follow it.
|
|
113
106
|
function executionGroups(messages) {
|
|
114
107
|
const groups = [];
|
|
@@ -43,6 +43,29 @@ export function latestActualUserInstructionIndex(messages) {
|
|
|
43
43
|
return -1;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The user instruction that began the request `index` belongs to; a steering
|
|
47
|
+
// message continues the request instead of starting one.
|
|
48
|
+
export function requestStart(messages, index) {
|
|
49
|
+
for (let i = index; i >= 0; i -= 1) {
|
|
50
|
+
if (!isActualUserInstructionMessage(messages[i])) continue;
|
|
51
|
+
if (messages[i]?.meta?.source !== 'steering') return i;
|
|
52
|
+
}
|
|
53
|
+
return latestActualUserInstructionIndex(messages.slice(0, index + 1));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A tool call's arguments as an object, whether stored parsed or as JSON text.
|
|
57
|
+
export function callArguments(call) {
|
|
58
|
+
let args = call?.arguments;
|
|
59
|
+
if (typeof args === 'string') {
|
|
60
|
+
try {
|
|
61
|
+
args = JSON.parse(args);
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return args && typeof args === 'object' ? args : {};
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
export function latestActualUserInstructionMessage(messages) {
|
|
47
70
|
const index = latestActualUserInstructionIndex(messages);
|
|
48
71
|
return index >= 0 ? { ...messages[index] } : null;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// Observations that a later one of the same browser page
|
|
2
|
-
// replaced. Such a result's refs
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// keeps every
|
|
1
|
+
// Observations that a later one of the same browser page, desktop window,
|
|
2
|
+
// visualize tab (inspect / check) or image file replaced. Such a result's refs
|
|
3
|
+
// have expired, and its element list and pixels describe a state that no
|
|
4
|
+
// longer exists. Only compaction drops them: between compactions the
|
|
5
|
+
// transcript stays append-only so the provider cache prefix holds, and
|
|
6
|
+
// compaction rewrites that prefix anyway. The execution archive keeps every
|
|
7
|
+
// original.
|
|
8
|
+
import { callArguments, requestStart } from './messages.mjs';
|
|
9
|
+
|
|
7
10
|
const BROWSER_TOOLS = new Set(['browser', 'browser_devtools']);
|
|
8
11
|
const SNAPSHOT_LINE = /(?:^|\n)Snapshot:\s+((p\d+)-s\d+)\b/;
|
|
9
12
|
const TERMINAL_LINE = /(?:^|\n)Terminal:\s+(session-terminal:\S+)/;
|
|
@@ -52,13 +55,40 @@ function containsRun(newer, older) {
|
|
|
52
55
|
return false;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const VISUALIZE_READS = new Set(['inspect', 'check']);
|
|
59
|
+
const IMAGE_FILE = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
60
|
+
// What a superseded computer frame keeps besides its reference.
|
|
61
|
+
const FRAME_OUTCOME_KEYS = ['ok', 'action', 'actions', 'status', 'error', 'window_id'];
|
|
62
|
+
|
|
63
|
+
// A visualize read-out is the same observation only for the same tab, kind of
|
|
64
|
+
// read, viewport and scope: a mobile inspect does not replace a desktop one.
|
|
65
|
+
function visualizeKey(args) {
|
|
66
|
+
const input = args.input && typeof args.input === 'object' ? args.input : {};
|
|
67
|
+
if (!VISUALIZE_READS.has(args.action) || typeof input.tab !== 'string' || !input.tab) return null;
|
|
68
|
+
const scope = args.action === 'inspect' ? `:${JSON.stringify(input.viewport ?? null)}:${input.selector ?? ''}` : '';
|
|
69
|
+
return { key: `visualize:${args.action}:${input.tab}${scope}`, action: args.action, tab: input.tab };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// What one tool result observed, keyed by the page, window, tab or file it
|
|
73
|
+
// describes.
|
|
74
|
+
function observationOf(message, call) {
|
|
57
75
|
if (message?.role !== 'tool' || message.toolKind === 'error') return null;
|
|
76
|
+
const toolName = String(call?.name || '');
|
|
58
77
|
const parts = contentParts(message.content);
|
|
78
|
+
if (toolName === 'read') {
|
|
79
|
+
const args = callArguments(call);
|
|
80
|
+
const path = typeof args.path === 'string' ? args.path : args.file_path;
|
|
81
|
+
if (typeof path !== 'string' || !IMAGE_FILE.test(path) || !parts.some(isImagePart)) return null;
|
|
82
|
+
return { key: `image:${path.replace(/\\/g, '/')}`, imagePath: path, requestScoped: true };
|
|
83
|
+
}
|
|
59
84
|
const textIndex = parts.findIndex((part) => part?.type === 'text' && !isImagePart(part));
|
|
60
85
|
if (textIndex < 0) return null;
|
|
61
86
|
const text = String(parts[textIndex].text);
|
|
87
|
+
if (toolName === 'visualize') {
|
|
88
|
+
if (/^\[Visualize \w+ of tab \S+ superseded:/.test(text)) return null;
|
|
89
|
+
const read = visualizeKey(callArguments(call));
|
|
90
|
+
return read ? { key: read.key, parts, textIndex, visualize: read, requestScoped: true } : null;
|
|
91
|
+
}
|
|
62
92
|
if (BROWSER_TOOLS.has(toolName)) {
|
|
63
93
|
const snapshot = SNAPSHOT_LINE.exec(text);
|
|
64
94
|
return snapshot ? { key: `browser:${snapshot[2]}`, parts, textIndex, text, snapshot } : null;
|
|
@@ -82,40 +112,90 @@ function observationOf(message, toolName) {
|
|
|
82
112
|
const frame = value?.observation && typeof value.observation === 'object' ? value.observation : value;
|
|
83
113
|
const windowId = frame?.window_id || value?.window_id;
|
|
84
114
|
const observed = Array.isArray(frame?.elements) || frame?.ocr !== undefined || parts.some(isImagePart);
|
|
85
|
-
|
|
115
|
+
if (windowId && observed) return { key: `computer:${windowId}`, parts, textIndex, value, frame };
|
|
116
|
+
// A stub an earlier compaction wrote in the fuller format.
|
|
117
|
+
const slim =
|
|
118
|
+
typeof frame?.superseded === 'string' &&
|
|
119
|
+
Object.keys(frame).some((name) => name !== 'superseded' && !FRAME_OUTCOME_KEYS.includes(name));
|
|
120
|
+
return slim ? { stub: true, parts, textIndex, value, frame, reference: frame.superseded } : null;
|
|
86
121
|
}
|
|
87
122
|
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
// Transcript indices whose results are still in use and must stay verbatim:
|
|
124
|
+
// the latest tool-call group (the batch the session continues from, which
|
|
125
|
+
// compaction always retains) and any group with a call that has no result yet.
|
|
126
|
+
function protectedObservationIndices(messages) {
|
|
127
|
+
const protectedIndices = new Set();
|
|
128
|
+
const protect = (from, to) => {
|
|
129
|
+
for (let index = from; index < to; index += 1) protectedIndices.add(index);
|
|
130
|
+
};
|
|
131
|
+
let latest = null;
|
|
132
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
133
|
+
const message = messages[index];
|
|
134
|
+
if (message?.role !== 'assistant' || !message.toolCalls?.length) continue;
|
|
135
|
+
const start = index;
|
|
136
|
+
const answered = new Set();
|
|
137
|
+
while (messages[index + 1]?.role === 'tool') {
|
|
138
|
+
index += 1;
|
|
139
|
+
answered.add(messages[index].toolCallId);
|
|
140
|
+
}
|
|
141
|
+
latest = [start, index + 1];
|
|
142
|
+
if (message.toolCalls.some((call) => !answered.has(call?.id))) protect(...latest);
|
|
143
|
+
}
|
|
144
|
+
if (latest) protect(...latest);
|
|
145
|
+
return protectedIndices;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Tool results that a later observation of the same page, window, tab or
|
|
149
|
+
* file replaced, keyed by transcript index. Protected results are never
|
|
150
|
+
* listed, though they still count as the newer observation of what they
|
|
151
|
+
* describe. An image file read or visualize read-out is also kept for the
|
|
152
|
+
* whole active request (everything since the last user instruction), since
|
|
153
|
+
* the request may compare it with a later one; once a newer user message
|
|
154
|
+
* exists it is replaceable. */
|
|
90
155
|
export function staleObservations(messages) {
|
|
91
|
-
const
|
|
156
|
+
const protectedIndices = protectedObservationIndices(messages);
|
|
157
|
+
const activeRequest = Math.max(requestStart(messages, messages.length - 1), 0);
|
|
158
|
+
const calls = new Map();
|
|
92
159
|
for (const message of messages) {
|
|
93
160
|
if (message?.role !== 'assistant' || !Array.isArray(message.toolCalls)) continue;
|
|
94
|
-
for (const call of message.toolCalls) if (call?.id)
|
|
161
|
+
for (const call of message.toolCalls) if (call?.id) calls.set(call.id, call);
|
|
95
162
|
}
|
|
96
163
|
const newest = new Set();
|
|
97
164
|
const terminalReads = new Map();
|
|
98
165
|
const stale = new Map();
|
|
99
166
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
100
|
-
const found = observationOf(messages[index],
|
|
167
|
+
const found = observationOf(messages[index], calls.get(messages[index]?.toolCallId));
|
|
101
168
|
if (!found) continue;
|
|
102
|
-
|
|
169
|
+
const replaceable = !protectedIndices.has(index) && !(found.requestScoped && index >= activeRequest);
|
|
170
|
+
if (found.stub) {
|
|
171
|
+
if (replaceable) stale.set(index, found);
|
|
172
|
+
} else if (found.body) {
|
|
103
173
|
const newer = terminalReads.get(found.key) ?? [];
|
|
104
|
-
if (newer.some((body) => containsRun(body, found.body))) stale.set(index, found);
|
|
174
|
+
if (replaceable && newer.some((body) => containsRun(body, found.body))) stale.set(index, found);
|
|
105
175
|
newer.push(found.body);
|
|
106
176
|
terminalReads.set(found.key, newer);
|
|
107
|
-
} else if (newest.has(found.key))
|
|
108
|
-
|
|
177
|
+
} else if (newest.has(found.key)) {
|
|
178
|
+
if (replaceable) stale.set(index, found);
|
|
179
|
+
} else newest.add(found.key);
|
|
109
180
|
}
|
|
110
181
|
return stale;
|
|
111
182
|
}
|
|
112
183
|
|
|
113
184
|
/** The result without its replaced observation. A browser result keeps what
|
|
114
185
|
* precedes its snapshot (effect notes, a script's value) and a computer
|
|
115
|
-
* result its action outcome;
|
|
186
|
+
* result its action outcome plus a reference; none keeps elements, OCR text,
|
|
187
|
+
* frame metadata or pixels. A visualize read-out or image file read becomes a
|
|
188
|
+
* one-line placeholder naming what it was and how to get it again. */
|
|
116
189
|
export function supersedeObservation(message, found, origin) {
|
|
117
190
|
let text;
|
|
118
|
-
if (found.
|
|
191
|
+
if (found.imagePath) {
|
|
192
|
+
text = `[Image read of ${found.imagePath} superseded: a newer read of this file follows. Read the file again to view it. ${origin}]`;
|
|
193
|
+
return { ...message, content: withParts(message.content, [{ type: 'text', text }]) };
|
|
194
|
+
}
|
|
195
|
+
if (found.visualize) {
|
|
196
|
+
const { action, tab } = found.visualize;
|
|
197
|
+
text = `[Visualize ${action} of tab ${tab} superseded: a newer ${action} of this tab follows. Run visualize ${action} on the tab again to get it. ${origin}]`;
|
|
198
|
+
} else if (found.terminal) {
|
|
119
199
|
text = `[Terminal ${found.terminal} read superseded: a newer read of this tab follows. ${origin}]`;
|
|
120
200
|
} else if (found.snapshot) {
|
|
121
201
|
const { snapshot } = found;
|
|
@@ -126,9 +206,9 @@ export function supersedeObservation(message, found, origin) {
|
|
|
126
206
|
const note = `[Snapshot ${snapshot[1]} superseded: a newer snapshot of this page follows. ${origin}]`;
|
|
127
207
|
text = before ? `${before}\n\n${note}` : note;
|
|
128
208
|
} else {
|
|
129
|
-
const frame = {
|
|
130
|
-
|
|
131
|
-
|
|
209
|
+
const frame = {};
|
|
210
|
+
for (const name of FRAME_OUTCOME_KEYS) if (found.frame[name] !== undefined) frame[name] = found.frame[name];
|
|
211
|
+
frame.superseded = found.reference ?? `a newer observation of this window follows. ${origin}`;
|
|
132
212
|
text = JSON.stringify(found.frame === found.value ? frame : { ...found.value, observation: frame });
|
|
133
213
|
}
|
|
134
214
|
const parts = found.parts.flatMap((part, index) => {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
textByteLength,
|
|
13
13
|
} from './text-utils.mjs';
|
|
14
14
|
import {
|
|
15
|
+
callArguments,
|
|
15
16
|
contextAckMessage,
|
|
16
17
|
isCompactConversationRow,
|
|
17
18
|
isProtectedContextAckMessage,
|
|
@@ -25,6 +26,7 @@ import { activeTurnContinuationMessage } from './continuation.mjs';
|
|
|
25
26
|
import { EXECUTION_RECOVERY_SOURCE, buildExecutionTail } from './execution-tail.mjs';
|
|
26
27
|
import { latestSkillBodies, skillMessageText } from '../../context/skill-state.mjs';
|
|
27
28
|
import { effectiveBudget } from './budget.mjs';
|
|
29
|
+
import { parseMcpToolName } from '../../../../shared/tool-primitives.mjs';
|
|
28
30
|
import { stripRuntimeUserContext, withRuntimeUserContext } from '../runtime-user-context.mjs';
|
|
29
31
|
import { carryMessageId, messageIdOf } from '../message-ids.mjs';
|
|
30
32
|
import { normalizeIngestRole, shouldExcludeIngestMessage } from '../../../../memory/lib/session-ingest.mjs';
|
|
@@ -70,12 +72,23 @@ function splitFreshSource(messages) {
|
|
|
70
72
|
break;
|
|
71
73
|
}
|
|
72
74
|
}
|
|
75
|
+
// Where the previous Compact's skill-use window opens: `live` index after
|
|
76
|
+
// the history it kept (see SKILL_WINDOW_SOURCE). Without a marker the whole
|
|
77
|
+
// transcript is the window.
|
|
78
|
+
const isKept = (message) => !isSummaryMessage(message) && message?.meta?.source !== SKILL_WINDOW_SOURCE;
|
|
79
|
+
const markerAt = conversation.findLastIndex((message) => message?.meta?.source === SKILL_WINDOW_SOURCE);
|
|
80
|
+
const span = conversation[markerAt]?.meta?.span;
|
|
81
|
+
const skillWindowStart =
|
|
82
|
+
markerAt >= 0 && Number.isInteger(span) && span >= 0
|
|
83
|
+
? conversation.slice(0, markerAt).filter(isKept).length + span
|
|
84
|
+
: 0;
|
|
73
85
|
const latestUserIndex = latestActualUserInstructionIndex(conversation);
|
|
74
86
|
return {
|
|
75
87
|
protectedPrefix,
|
|
76
88
|
previousSummary: previousSummaryMessage?.content || null,
|
|
77
89
|
previousSummaryMessage,
|
|
78
90
|
previousAck,
|
|
91
|
+
skillWindowStart,
|
|
79
92
|
// Budgeting, AI input and rule-only retention use this same projection.
|
|
80
93
|
// Keep current-turn reminders; remove only proven runtime additions from
|
|
81
94
|
// older requests. Unmarked legacy text remains part of the summary budget.
|
|
@@ -86,7 +99,7 @@ function splitFreshSource(messages) {
|
|
|
86
99
|
.map((message, index) =>
|
|
87
100
|
index === latestUserIndex ? message : referenceMessageMedia(stripRuntimeUserContext(message))
|
|
88
101
|
)
|
|
89
|
-
.filter(
|
|
102
|
+
.filter(isKept),
|
|
90
103
|
sanitized,
|
|
91
104
|
};
|
|
92
105
|
}
|
|
@@ -135,9 +148,11 @@ function toolCallSummaryLine(call, result) {
|
|
|
135
148
|
// Arguments arrive redacted (splitFreshSource); the outcome is redacted
|
|
136
149
|
// here, before any cut can split a secret from its key.
|
|
137
150
|
const outcome = redactRawSecretString(contentText(result?.content)).trim();
|
|
138
|
-
// An ask_user result is the user's own answer: never cut.
|
|
151
|
+
// An ask_user or handoff_user result is the user's own answer: never cut.
|
|
139
152
|
const shown =
|
|
140
|
-
call.name === 'ask_user'
|
|
153
|
+
call.name === 'ask_user' || call.name === 'handoff_user'
|
|
154
|
+
? outcome
|
|
155
|
+
: boundedSummaryText(outcome.replace(/\s+/g, ' '), SUMMARY_TOOL_OUTCOME_CHARS);
|
|
141
156
|
return `[tool call] ${call.name}(${args}) → ${result ? shown || '(empty result)' : '(no result)'}`;
|
|
142
157
|
}
|
|
143
158
|
|
|
@@ -564,44 +579,128 @@ function currentSkillBodyMessage(message, current) {
|
|
|
564
579
|
|
|
565
580
|
const SKILL_RELOAD_SOURCE = 'compact-skill-reload';
|
|
566
581
|
|
|
567
|
-
|
|
582
|
+
// How many Compacts in a row a skill may go unused before even its reload
|
|
583
|
+
// line is dropped: a skill untouched that long belongs to finished work, and
|
|
584
|
+
// the model can still load it by name from the skill manifest.
|
|
585
|
+
export const SKILL_RELOAD_MAX_IDLE = 3;
|
|
586
|
+
|
|
587
|
+
const SKILL_TOOL_NAMES = new Set(['Skill', 'skill_view']);
|
|
588
|
+
|
|
589
|
+
function skillReloadReminder(name, idle = 0) {
|
|
568
590
|
return {
|
|
569
591
|
role: 'user',
|
|
570
|
-
content: `[mixdog-runtime] Skill "${name}" was active, but
|
|
571
|
-
meta: { source: SKILL_RELOAD_SOURCE, synthetic: true, skill: name },
|
|
592
|
+
content: `[mixdog-runtime] Skill "${name}" was active earlier, but Compact did not keep its instructions. Reload it with the Skill tool before relying on them.`,
|
|
593
|
+
meta: { source: SKILL_RELOAD_SOURCE, synthetic: true, skill: name, idle },
|
|
572
594
|
};
|
|
573
595
|
}
|
|
574
596
|
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
597
|
+
// Every Compact leaves this marker just ahead of the skills it restores, with
|
|
598
|
+
// `span`: how many messages follow it (restored skills and retained tail).
|
|
599
|
+
// All of them happened before that Compact, so the next one reads the skill
|
|
600
|
+
// use window from after them, whether or not a summary exists. The marker is
|
|
601
|
+
// not conversation: the next Compact removes it and writes a new one.
|
|
602
|
+
const SKILL_WINDOW_SOURCE = 'compact-skill-window';
|
|
603
|
+
|
|
604
|
+
function skillWindowMarker(span = 0) {
|
|
605
|
+
return {
|
|
606
|
+
role: 'user',
|
|
607
|
+
content: '[mixdog-runtime] Compact boundary.',
|
|
608
|
+
meta: { source: SKILL_WINDOW_SOURCE, synthetic: true, span },
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Restore the skills the session used since the previous Compact, each as the
|
|
613
|
+
// Skill tool would deliver it now. A skill is used when it was loaded or one of
|
|
614
|
+
// its linked tools ran in that window (`windowStart` onward); its linked
|
|
615
|
+
// tools are the native tool references of its loader pair and, when the
|
|
616
|
+
// resolver gives them, `tools`. `currentSkill(name)` (the caller's resolver)
|
|
617
|
+
// answers `{ body, native, summary, tools? }` for a skill that still loads in
|
|
618
|
+
// the session, or null for one switched off, outside its scope or removed:
|
|
619
|
+
// such a skill is restored neither as a body nor as a reload line, since its
|
|
620
|
+
// instructions are no longer in force. Without a resolver the transcript
|
|
621
|
+
// copies are kept.
|
|
581
622
|
// Never summarize/truncate operating instructions or displace the handoff
|
|
582
|
-
// and current task.
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
623
|
+
// and current task. Every other active skill, and a used one whose body does
|
|
624
|
+
// not fit, is left as a one-line reload line. The line counts the Compacts
|
|
625
|
+
// the skill went unused (`meta.idle`) and is dropped past SKILL_RELOAD_MAX_IDLE.
|
|
626
|
+
// Reload lines are charged to the same budget: every possible line is
|
|
627
|
+
// reserved before any body is restored, and restoring a body releases its own.
|
|
628
|
+
function restoreSkillBodies(live, skillBudget, retainedTail, currentSkill = null, windowStart = 0) {
|
|
587
629
|
const retainedCallIds = new Set(retainedTail.filter((m) => m?.role === 'tool').map((m) => m.toolCallId));
|
|
588
630
|
const current = (name) => (currentSkill ? currentSkill(name) : undefined);
|
|
589
631
|
const loads = (name) => current(name) !== null;
|
|
590
632
|
const transcriptBodies = latestSkillBodies(live);
|
|
591
633
|
const loaded = new Set(transcriptBodies.map((body) => body.name));
|
|
592
634
|
const bodies = transcriptBodies.filter((body) => loads(body.name));
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
635
|
+
// Reload lines from earlier Compacts: skill -> Compacts it went unused.
|
|
636
|
+
const earlier = new Map();
|
|
637
|
+
for (const m of live) {
|
|
638
|
+
if (m?.meta?.source !== SKILL_RELOAD_SOURCE || loaded.has(m.meta.skill) || !loads(m.meta.skill)) continue;
|
|
639
|
+
earlier.delete(m.meta.skill);
|
|
640
|
+
earlier.set(m.meta.skill, Number.isFinite(m.meta.idle) ? m.meta.idle : 0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// A loader pair can sit anywhere in the transcript (a restored body stays
|
|
644
|
+
// ahead of the retained tail's pair), so read them all.
|
|
645
|
+
const results = new Map(live.filter((m) => m?.role === 'tool').map((m) => [m.toolCallId, m]));
|
|
646
|
+
const loaderTools = new Map();
|
|
647
|
+
for (const message of live) {
|
|
648
|
+
for (const call of message?.role === 'assistant' ? message.toolCalls || [] : []) {
|
|
649
|
+
const references = results.get(call.id)?.nativeToolSearch?.toolReferences;
|
|
650
|
+
if (call.name !== 'Skill' || !references) continue;
|
|
651
|
+
const name = String(callArguments(call).name || '').trim();
|
|
652
|
+
loaderTools.set(name, [...(loaderTools.get(name) || []), ...references]);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const skillsOfTool = new Map();
|
|
656
|
+
for (const name of new Set([...earlier.keys(), ...bodies.map((body) => body.name)])) {
|
|
657
|
+
const skill = current(name);
|
|
658
|
+
const tools = [...(skill?.native?.toolReferences || []), ...(skill?.tools || []), ...(loaderTools.get(name) || [])];
|
|
659
|
+
for (const tool of tools) skillsOfTool.set(tool, (skillsOfTool.get(tool) || new Set()).add(name));
|
|
660
|
+
}
|
|
661
|
+
// Use since the previous Compact (from `windowStart`): the newest transcript
|
|
662
|
+
// position of a load or of a linked tool's call.
|
|
663
|
+
const lastUse = new Map();
|
|
664
|
+
const noteUse = (name, index) => lastUse.set(name, Math.max(lastUse.get(name) ?? -1, index));
|
|
665
|
+
live.forEach((message, index) => {
|
|
666
|
+
if (index < windowStart || message?.role !== 'assistant') return;
|
|
667
|
+
for (const call of message.toolCalls || []) {
|
|
668
|
+
const args = callArguments(call);
|
|
669
|
+
if (SKILL_TOOL_NAMES.has(call.name)) noteUse(String(args.name || '').trim(), index);
|
|
670
|
+
else {
|
|
671
|
+
const tool = call.name === 'call_tool' ? args.name : call.name;
|
|
672
|
+
const mcpServer = `mcp:${parseMcpToolName(tool)?.server}`;
|
|
673
|
+
for (const name of [...(skillsOfTool.get(tool) || []), ...(skillsOfTool.get(mcpServer) || [])]) {
|
|
674
|
+
noteUse(name, index);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
for (const { name, message } of bodies) {
|
|
680
|
+
const index = live.lastIndexOf(message);
|
|
681
|
+
if (index >= windowStart) noteUse(name, index);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Every active skill without a body ends as a reload line: an unused one
|
|
685
|
+
// ages by a Compact and goes once past the bound; one used (or whose body
|
|
686
|
+
// did not fit) starts over.
|
|
687
|
+
const reminders = new Map();
|
|
688
|
+
for (const [name, idle] of earlier) {
|
|
689
|
+
if (lastUse.has(name)) reminders.set(name, 0);
|
|
690
|
+
else if (idle < SKILL_RELOAD_MAX_IDLE) reminders.set(name, idle + 1);
|
|
691
|
+
}
|
|
692
|
+
const used = bodies.filter(({ name }) => lastUse.has(name)).sort((a, b) => lastUse.get(b.name) - lastUse.get(a.name));
|
|
693
|
+
const unused = bodies.filter(({ name }) => !lastUse.has(name));
|
|
694
|
+
|
|
695
|
+
const reminderCost = (name, idle = 0) => estimateMessagesTokens([skillReloadReminder(name, idle)]);
|
|
696
|
+
let reserved =
|
|
697
|
+
[...reminders].reduce((sum, [name, idle]) => sum + reminderCost(name, idle), 0) +
|
|
698
|
+
unused.reduce((sum, { name }) => sum + reminderCost(name, 1), 0) +
|
|
699
|
+
used.reduce((sum, { name }) => sum + reminderCost(name), 0);
|
|
700
|
+
const dropped = new Set();
|
|
602
701
|
const restoredSkills = [];
|
|
603
702
|
let skillTokens = 0;
|
|
604
|
-
for (const { name, message } of
|
|
703
|
+
for (const { name, message } of used) {
|
|
605
704
|
const skill = current(name);
|
|
606
705
|
const restored = [
|
|
607
706
|
...skillLoaderPair(live, live.lastIndexOf(message), name, retainedCallIds, skill),
|
|
@@ -610,24 +709,31 @@ function restoreSkillBodies(live, skillBudget, retainedTail, currentSkill = null
|
|
|
610
709
|
const cost = estimateMessagesTokens(restored);
|
|
611
710
|
const own = reminderCost(name);
|
|
612
711
|
if (skillTokens + cost + reserved - own > skillBudget) {
|
|
613
|
-
dropped.
|
|
712
|
+
dropped.add(name);
|
|
614
713
|
continue;
|
|
615
714
|
}
|
|
616
715
|
restoredSkills.unshift(...restored);
|
|
617
716
|
skillTokens += cost;
|
|
618
717
|
reserved -= own;
|
|
619
718
|
}
|
|
620
|
-
//
|
|
719
|
+
// In transcript order, after the lines of earlier Compacts.
|
|
720
|
+
for (const { name } of bodies) {
|
|
721
|
+
if (!lastUse.has(name)) reminders.set(name, 1);
|
|
722
|
+
else if (dropped.has(name)) reminders.set(name, 0);
|
|
723
|
+
}
|
|
724
|
+
// Newest skill first; only a budget too small for the lines themselves
|
|
621
725
|
// leaves one out.
|
|
622
|
-
const pending = [...
|
|
726
|
+
const pending = [...reminders];
|
|
623
727
|
const reminded = new Set();
|
|
624
|
-
for (const name of pending.toReversed()) {
|
|
625
|
-
const cost = reminderCost(name);
|
|
728
|
+
for (const [name, idle] of pending.toReversed()) {
|
|
729
|
+
const cost = reminderCost(name, idle);
|
|
626
730
|
if (skillTokens + cost > skillBudget) continue;
|
|
627
731
|
reminded.add(name);
|
|
628
732
|
skillTokens += cost;
|
|
629
733
|
}
|
|
630
|
-
restoredSkills.push(
|
|
734
|
+
restoredSkills.push(
|
|
735
|
+
...pending.filter(([name]) => reminded.has(name)).map(([name, idle]) => skillReloadReminder(name, idle))
|
|
736
|
+
);
|
|
631
737
|
return { restoredSkills, skillTokens };
|
|
632
738
|
}
|
|
633
739
|
|
|
@@ -864,15 +970,33 @@ export function freshContextCompactMessages(messages, budgetTokens, options = {}
|
|
|
864
970
|
const summaryContent = String(summaryMessage?.content || '');
|
|
865
971
|
assertHandoffFits(summaryMessage, handoff.room);
|
|
866
972
|
const skillBudget = freshSkillBudget(targetBudget, budget, [...stableHead, ...tail.volatileTail]);
|
|
973
|
+
// Restored skills carry the window marker, which is charged to their budget.
|
|
974
|
+
const markerCost = estimateMessagesTokens([skillWindowMarker()]);
|
|
867
975
|
const skills = {
|
|
868
|
-
...restoreSkillBodies(
|
|
976
|
+
...restoreSkillBodies(
|
|
977
|
+
source.live,
|
|
978
|
+
Math.max(0, skillBudget - markerCost),
|
|
979
|
+
tail.volatileTail,
|
|
980
|
+
opts.currentSkill,
|
|
981
|
+
source.skillWindowStart
|
|
982
|
+
),
|
|
869
983
|
skillBudget,
|
|
870
984
|
};
|
|
871
|
-
|
|
872
|
-
const
|
|
985
|
+
// A session with no skill state has no use to window: no marker.
|
|
986
|
+
const marker = skills.restoredSkills.length ? [skillWindowMarker()] : [];
|
|
987
|
+
const stablePrefixMessages = [...stableHead, ...marker, ...skills.restoredSkills];
|
|
988
|
+
const compacted = rebaseCompactedEffortConfiguration(
|
|
873
989
|
baseSanitized,
|
|
874
990
|
reconcileDedupStubs(sanitizeToolPairs([...stablePrefixMessages, ...tail.volatileTail]))
|
|
875
991
|
);
|
|
992
|
+
// The next Compact reads everything after the marker as history, not as use.
|
|
993
|
+
const markerAt = compacted.findLastIndex((message) => message?.meta?.source === SKILL_WINDOW_SOURCE);
|
|
994
|
+
const result =
|
|
995
|
+
markerAt < 0
|
|
996
|
+
? compacted
|
|
997
|
+
: compacted.map((message, index) =>
|
|
998
|
+
index === markerAt ? skillWindowMarker(compacted.length - markerAt - 1) : message
|
|
999
|
+
);
|
|
876
1000
|
const finalTokens = estimateMessagesTokens(result);
|
|
877
1001
|
if (finalTokens > budget) {
|
|
878
1002
|
throw new Error(`freshContextCompactMessages: compacted result exceeds budget=${budget} (result=${finalTokens})`);
|
|
@@ -581,8 +581,20 @@ function reasoningBreakdown(message, fullTextTokens) {
|
|
|
581
581
|
};
|
|
582
582
|
}
|
|
583
583
|
}
|
|
584
|
+
// Price each reasoning item that is actually sent (a non-empty replay
|
|
585
|
+
// replaces content) at its own cost; the whole-minus-visible difference only
|
|
586
|
+
// caps the sum so the rows never exceed the total.
|
|
587
|
+
const sent = message.providerReplay?.items?.length ? message.providerReplay.items.filter(isReasoningBlock) : blocks;
|
|
584
588
|
return {
|
|
585
|
-
tokens: blocks.length
|
|
589
|
+
tokens: blocks.length
|
|
590
|
+
? Math.max(
|
|
591
|
+
0,
|
|
592
|
+
Math.min(
|
|
593
|
+
sent.reduce((sum, block) => sum + estimateTokens(nativeBlocksEstimateText(block)), 0),
|
|
594
|
+
fullTextTokens() - messageTextTokens(visible)
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
: 0,
|
|
586
598
|
blocks,
|
|
587
599
|
message: visible,
|
|
588
600
|
};
|