@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +4 -0
- package/dist/cc-orchestrator/agent-adapter.js +11 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +65 -5
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +265 -50
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +98 -15
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +16 -2
- package/dist/context/manager.js +103 -19
- package/dist/credentials/access.d.ts +57 -0
- package/dist/credentials/access.js +185 -0
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/oauth.d.ts +20 -0
- package/dist/credentials/oauth.js +114 -0
- package/dist/credentials/store.d.ts +1 -0
- package/dist/credentials/store.js +3 -1
- package/dist/credentials/types.d.ts +49 -3
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +23 -3
- package/dist/engine/engine.js +380 -255
- package/dist/engine/goal.d.ts +17 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +398 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +37 -2
- package/dist/engine/turn-loop.js +228 -43
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/hooks/goal-stop-hook.d.ts +33 -1
- package/dist/hooks/goal-stop-hook.js +202 -34
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +30 -13
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +30 -0
- package/dist/protocol/server.js +247 -47
- package/dist/protocol/types.d.ts +49 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +224 -4
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
- package/dist/tool-system/builtin/background-jobs.js +116 -7
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
- package/dist/tool-system/builtin/drive-claude-code.js +589 -48
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +50 -22
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +41 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +31 -6
- package/dist/tool-system/mcp-manager.js +193 -78
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +78 -11
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
package/dist/engine/engine.js
CHANGED
|
@@ -23,7 +23,7 @@ import { PermissionClassifier, HeadlessApprovalBackend, AutoApprovalBackend, Int
|
|
|
23
23
|
import { HookRegistry } from "../hooks/registry.js";
|
|
24
24
|
import { wrapHookMessages } from "../hooks/inject.js";
|
|
25
25
|
import { createGoalStopHook } from "../hooks/goal-stop-hook.js";
|
|
26
|
-
import { normalizeGoal, resolveGoalSetAt, resolveMaxTurns, resolveMaxStopBlocks, } from "./goal.js";
|
|
26
|
+
import { normalizeGoal, resolveGoalSetAt, resolveMaxTurns, resolveMaxStopBlocks, isSameGoalInstance, } from "./goal.js";
|
|
27
27
|
import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
|
|
28
28
|
import { pluginAgentDirs } from "../plugins/installer/loadPluginAgents.js";
|
|
29
29
|
import { patchOrphanedToolUses } from "./patch-orphaned-tools.js";
|
|
@@ -34,34 +34,35 @@ import { PLAN_MODE_ALLOWED_TOOLS } from "../tool-system/plan-mode-allowlist.js";
|
|
|
34
34
|
import { PromptComposer } from "../prompt/composer.js";
|
|
35
35
|
import { SessionManager } from "../session/session-manager.js";
|
|
36
36
|
import { ModelFacade } from "./model-facade.js";
|
|
37
|
-
import { logger,
|
|
37
|
+
import { logger, runWithSid, getCurrentSid } from "../logging/logger.js";
|
|
38
38
|
import { recordSessionStart, recordSessionEnd } from "../logging/session-recorder.js";
|
|
39
39
|
import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-messages.js";
|
|
40
40
|
import { TurnLoop } from "./turn-loop.js";
|
|
41
41
|
import { MCPManager } from "../tool-system/mcp-manager.js";
|
|
42
42
|
import { SettingsManager, userHome } from "../settings/manager.js";
|
|
43
|
-
import {
|
|
43
|
+
import { getCredentialAccess } from "../credentials/access.js";
|
|
44
44
|
import { isFeatureEnabled, resolveFeatureFlags, } from "../settings/feature-flags.js";
|
|
45
|
-
import { effectiveDisabledList, effectiveBuiltinLists
|
|
45
|
+
import { effectiveDisabledList, effectiveBuiltinLists } from "../capability-control/overlay.js";
|
|
46
46
|
import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
|
|
47
47
|
import { FileHistory } from "../session/file-history.js";
|
|
48
48
|
import { patchBackupTargets } from "../tool-system/builtin/apply-patch/backup-targets.js";
|
|
49
49
|
import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
50
|
-
import { resolveAgentPreset, resolveBuiltinToolNames
|
|
50
|
+
import { resolveAgentPreset, resolveBuiltinToolNames } from "../preset/index.js";
|
|
51
51
|
import { ModelPool } from "../llm/model-pool.js";
|
|
52
52
|
import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
|
|
53
53
|
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
54
54
|
import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
|
|
55
55
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
56
|
-
import { parseTaskWithImages } from "./parse-task.js";
|
|
57
|
-
import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, collectAttachedImagePaths, } from "./image-policy.js";
|
|
58
|
-
import { tryCompressImages } from "./image-compression.js";
|
|
59
56
|
import { buildSessionTitle } from "./session-title.js";
|
|
60
|
-
import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
61
57
|
import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
62
58
|
import { runDreamConsolidation } from "../services/dream-consolidation.js";
|
|
63
|
-
import {
|
|
59
|
+
import { buildRunUserMessageContent, prepareRunImageInput } from "./run-image-input.js";
|
|
60
|
+
import { join } from "node:path";
|
|
64
61
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
62
|
+
const CACHE_READ_DROP_MIN_PREVIOUS_TOKENS = 100;
|
|
63
|
+
const CACHE_READ_DROP_MAX_CURRENT_TOKENS = 64;
|
|
64
|
+
const CACHE_READ_DROP_RATIO = 0.1;
|
|
65
|
+
const CACHE_READ_DIAGNOSTIC_MAX_SESSIONS = 256;
|
|
65
66
|
/**
|
|
66
67
|
* Build ScanOptions.compatFileNames from the user's instruction compat toggles.
|
|
67
68
|
* Primary file name stays hard-wired to CODESHELL.md (not exposed). Turning a
|
|
@@ -129,7 +130,7 @@ export function resolveChildLlm(modelKey, pool, parentLlm) {
|
|
|
129
130
|
* Names in `disabledAgents` are filtered out so the LLM never sees them.
|
|
130
131
|
*/
|
|
131
132
|
/**
|
|
132
|
-
* Resolve the working directory for a run. Precedence:
|
|
133
|
+
* Resolve the working directory for a run. Precedence for legacy sessions:
|
|
133
134
|
* options.cwd > resumed session's state.cwd > config.cwd > process.cwd()
|
|
134
135
|
*
|
|
135
136
|
* The session-cwd tier is what stops a project-bound session from being
|
|
@@ -265,6 +266,7 @@ export class Engine {
|
|
|
265
266
|
* LLM response arrives.
|
|
266
267
|
*/
|
|
267
268
|
ctxOverheadBySid = new Map();
|
|
269
|
+
lastCacheReadBySid = new Map();
|
|
268
270
|
/**
|
|
269
271
|
* Step-gap steering queue (per sessionId, in-memory). Host pushes user
|
|
270
272
|
* messages here via enqueueSteer while a run is in flight; the turn loop
|
|
@@ -445,6 +447,7 @@ export class Engine {
|
|
|
445
447
|
new ToolRegistry({
|
|
446
448
|
builtinTools: resolveBuiltinToolNames({
|
|
447
449
|
preset: this.preset.name,
|
|
450
|
+
host: config.builtinToolHost,
|
|
448
451
|
enabledBuiltinTools: builtinLists.enabledBuiltinTools,
|
|
449
452
|
disabledBuiltinTools: builtinLists.disabledBuiltinTools,
|
|
450
453
|
}),
|
|
@@ -635,6 +638,10 @@ export class Engine {
|
|
|
635
638
|
setBrowserBridge(bridge) {
|
|
636
639
|
this.config.browserBridge = bridge;
|
|
637
640
|
}
|
|
641
|
+
/** Inject the host-backed workspace bridge after construction. */
|
|
642
|
+
setWorkspaceBridge(bridge) {
|
|
643
|
+
this.config.workspaceBridge = bridge;
|
|
644
|
+
}
|
|
638
645
|
/**
|
|
639
646
|
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
640
647
|
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
@@ -648,7 +655,7 @@ export class Engine {
|
|
|
648
655
|
* the queued draft) and is the handle `unsteer` uses to revoke a still-pending
|
|
649
656
|
* entry. A blank id is tolerated but means the entry can't be revoked.
|
|
650
657
|
*/
|
|
651
|
-
enqueueSteer(sessionId, text, id = "", clientMessageId) {
|
|
658
|
+
enqueueSteer(sessionId, text, id = "", clientMessageId, attachments) {
|
|
652
659
|
const q = this.steerQueueBySid.get(sessionId) ?? [];
|
|
653
660
|
const entryId = id || `steer-${q.length}`;
|
|
654
661
|
if (!sessionId)
|
|
@@ -660,12 +667,13 @@ export class Engine {
|
|
|
660
667
|
sessionId,
|
|
661
668
|
id: entryId,
|
|
662
669
|
clientMessageId,
|
|
670
|
+
attachmentCount: attachments?.length ?? 0,
|
|
663
671
|
activeRunSessionId: activeRunSessionId ?? null,
|
|
664
672
|
queueLength: q.length,
|
|
665
673
|
});
|
|
666
674
|
return { accepted: false, id: entryId };
|
|
667
675
|
}
|
|
668
|
-
const next = enqueueSteerItem(q, entryId, text, clientMessageId);
|
|
676
|
+
const next = enqueueSteerItem(q, entryId, text, clientMessageId, attachments);
|
|
669
677
|
if (next === q)
|
|
670
678
|
return { accepted: false, id: entryId }; // blank text dropped
|
|
671
679
|
this.steerQueueBySid.set(sessionId, next);
|
|
@@ -673,6 +681,7 @@ export class Engine {
|
|
|
673
681
|
sessionId,
|
|
674
682
|
id: entryId,
|
|
675
683
|
clientMessageId,
|
|
684
|
+
attachmentCount: attachments?.length ?? 0,
|
|
676
685
|
activeRunSessionId,
|
|
677
686
|
queueLength: next.length,
|
|
678
687
|
});
|
|
@@ -709,6 +718,13 @@ export class Engine {
|
|
|
709
718
|
});
|
|
710
719
|
return drained;
|
|
711
720
|
}
|
|
721
|
+
/** Put failed steer preparation back ahead of messages queued while it was being prepared. */
|
|
722
|
+
restoreSteer(sessionId, items) {
|
|
723
|
+
if (items.length === 0)
|
|
724
|
+
return;
|
|
725
|
+
const queued = this.steerQueueBySid.get(sessionId) ?? [];
|
|
726
|
+
this.steerQueueBySid.set(sessionId, [...items, ...queued]);
|
|
727
|
+
}
|
|
712
728
|
/** Wire the cookie→browser injection callback (InjectCredential tool). Same
|
|
713
729
|
* post-construction injection model as setBrowserBridge. */
|
|
714
730
|
setInjectCredential(fn) {
|
|
@@ -735,20 +751,45 @@ export class Engine {
|
|
|
735
751
|
* Run a task from start to finish.
|
|
736
752
|
*/
|
|
737
753
|
async run(task, options) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
// loading its own agents/settings/memory even if the host's UI repo
|
|
741
|
-
// selection has drifted to null. Only probe on omission — an explicit cwd
|
|
742
|
-
// always wins, and a fresh session has nothing to recover.
|
|
743
|
-
const sessionCwd = options?.cwd === undefined && options?.sessionId
|
|
744
|
-
? this.sessionManager.readCwd(options.sessionId)
|
|
754
|
+
const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
|
|
755
|
+
? await this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
745
756
|
: undefined;
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
757
|
+
if (workspaceResume && !workspaceResume.ok) {
|
|
758
|
+
return {
|
|
759
|
+
text: `ERROR: ${workspaceResume.message}`,
|
|
760
|
+
reason: "completed",
|
|
761
|
+
sessionId: options.sessionId,
|
|
762
|
+
turnCount: 0,
|
|
763
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (workspaceResume?.ok &&
|
|
767
|
+
workspaceResume.reason === "worktree_missing_branch_gone" &&
|
|
768
|
+
workspaceResume.message) {
|
|
769
|
+
return {
|
|
770
|
+
text: workspaceResume.message,
|
|
771
|
+
reason: "completed",
|
|
772
|
+
sessionId: options.sessionId,
|
|
773
|
+
turnCount: 0,
|
|
774
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
// Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
|
|
778
|
+
// passes a stale cwd. Legacy sessions without workspace keep the historical
|
|
779
|
+
// explicit-cwd precedence for backward compatibility.
|
|
780
|
+
const workspaceCwd = workspaceResume?.ok && workspaceResume.reason !== "legacy" ? workspaceResume.cwd : undefined;
|
|
781
|
+
const sessionCwd = workspaceCwd === undefined && options?.cwd === undefined && options?.sessionId
|
|
782
|
+
? workspaceResume?.ok
|
|
783
|
+
? workspaceResume.cwd
|
|
784
|
+
: this.sessionManager.readCwd(options.sessionId)
|
|
785
|
+
: undefined;
|
|
786
|
+
const cwd = workspaceCwd ??
|
|
787
|
+
resolveRunCwd({
|
|
788
|
+
optionCwd: options?.cwd,
|
|
789
|
+
sessionCwd,
|
|
790
|
+
configCwd: this.config.cwd,
|
|
791
|
+
processCwd: process.cwd(),
|
|
792
|
+
});
|
|
752
793
|
// Wrap the caller's onStream so we can intercept `task_update`
|
|
753
794
|
// events emitted by TodoWrite and keep an in-engine snapshot.
|
|
754
795
|
// TaskGuard reads this snapshot at turn end to decide whether to
|
|
@@ -776,115 +817,16 @@ export class Engine {
|
|
|
776
817
|
};
|
|
777
818
|
if (options)
|
|
778
819
|
options.onStream = wrappedOnStream;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
let parsedTask;
|
|
790
|
-
try {
|
|
791
|
-
parsedTask = parseTaskWithImages(task);
|
|
792
|
-
}
|
|
793
|
-
catch (err) {
|
|
794
|
-
const msg = err.message;
|
|
795
|
-
logger.warn("engine.run.image_parse_failed", { error: msg });
|
|
796
|
-
return {
|
|
797
|
-
text: `ERROR: image attachment is malformed (${msg}). Drop the image and try again, or re-attach it.`,
|
|
798
|
-
reason: "image_error",
|
|
799
|
-
sessionId: options?.sessionId ?? "image-parse-failed",
|
|
800
|
-
turnCount: 0,
|
|
801
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
if (parsedTask.hasImages) {
|
|
805
|
-
const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
|
|
806
|
-
if (!cap.supportsVision) {
|
|
807
|
-
logger.warn("engine.run.vision_not_supported", {
|
|
808
|
-
provider: this.config.llm.provider,
|
|
809
|
-
model: this.config.llm.model,
|
|
810
|
-
imageCount: parsedTask.images.length,
|
|
811
|
-
});
|
|
812
|
-
return {
|
|
813
|
-
text: `ERROR: model "${this.config.llm.model}" does not accept image input. ` +
|
|
814
|
-
`Switch to a vision-capable model (e.g. gpt-4o, claude-sonnet, gemini-1.5-pro) and resend.`,
|
|
815
|
-
reason: "image_error",
|
|
816
|
-
sessionId: options?.sessionId ?? "vision-not-supported",
|
|
817
|
-
turnCount: 0,
|
|
818
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
// Size gate. Hosts (desktop renderer, TUI) are expected to
|
|
822
|
-
// pre-compress to IMAGE_TARGETS — if they didn't, we fail the turn
|
|
823
|
-
// fast with a clear message instead of letting the OpenAI client
|
|
824
|
-
// grind through three 16-second "Connection error" retries on a
|
|
825
|
-
// 4 MB body. See `image-policy.ts` for the rationale and limits.
|
|
826
|
-
let verdict = enforceImagePolicy(parsedTask.images);
|
|
827
|
-
if (!verdict.ok && verdict.code === "image_too_large") {
|
|
828
|
-
// One image blew the per-image cap. Try the engine-side
|
|
829
|
-
// compressor (jimp-backed when installed; no-op otherwise) so
|
|
830
|
-
// TUI / MCP paths that lack a host-side resize don't fail
|
|
831
|
-
// outright on a screenshot they could have rescaled. The
|
|
832
|
-
// re-check below is what decides whether we proceed.
|
|
833
|
-
const compressed = await tryCompressImages(parsedTask.images);
|
|
834
|
-
if (compressed.anyCompressed) {
|
|
835
|
-
parsedTask.images = compressed.images;
|
|
836
|
-
logger.info("engine.run.image_compressed", {
|
|
837
|
-
before: verdict.offender?.bytes,
|
|
838
|
-
after: compressed.images.reduce((s, i) => s + byteLengthFromBase64(i.base64), 0),
|
|
839
|
-
});
|
|
840
|
-
verdict = enforceImagePolicy(parsedTask.images);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
// After compression, anything still over the per-image cap is
|
|
844
|
-
// dropped with a textual placeholder instead of failing the
|
|
845
|
-
// turn (TODO-week.md #9e). The "5MB brick session" failure
|
|
846
|
-
// mode from Claude Code (research doc §A) was the case where a
|
|
847
|
-
// poisoned image entered history and every subsequent request
|
|
848
|
-
// re-sent it; placeholders keep history clean while letting
|
|
849
|
-
// the rest of the turn run.
|
|
850
|
-
if (!verdict.ok && verdict.code === "image_too_large") {
|
|
851
|
-
const drop = dropOversizedImages(parsedTask.images);
|
|
852
|
-
if (drop.droppedCount > 0) {
|
|
853
|
-
parsedTask.images = drop.kept;
|
|
854
|
-
parsedTask.hasImages = drop.kept.length > 0;
|
|
855
|
-
parsedTask.text = drop.placeholder + "\n\n" + parsedTask.text;
|
|
856
|
-
logger.warn("engine.run.image_dropped", {
|
|
857
|
-
droppedCount: drop.droppedCount,
|
|
858
|
-
keptCount: drop.kept.length,
|
|
859
|
-
});
|
|
860
|
-
verdict = enforceImagePolicy(parsedTask.images);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (!verdict.ok) {
|
|
864
|
-
// Cumulative / count caps can't be rescued by per-image
|
|
865
|
-
// dropping (well — too_many_images could trim by FIFO, but
|
|
866
|
-
// that's a bigger UX call than we want to make silently).
|
|
867
|
-
// Refuse the turn with the policy message.
|
|
868
|
-
logger.warn("engine.run.image_policy_failed", {
|
|
869
|
-
code: verdict.code,
|
|
870
|
-
imageCount: verdict.totals.imageCount,
|
|
871
|
-
totalBytes: verdict.totals.totalBytes,
|
|
872
|
-
offender: verdict.offender,
|
|
873
|
-
});
|
|
874
|
-
return {
|
|
875
|
-
text: `ERROR: ${verdict.message}`,
|
|
876
|
-
reason: "image_error",
|
|
877
|
-
sessionId: options?.sessionId ?? `image-policy-${verdict.code}`,
|
|
878
|
-
turnCount: 0,
|
|
879
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
// For downstream noise-detection + transcript persistence we want the
|
|
884
|
-
// *text* portion only — base64 bytes count as "noise" by the heuristic
|
|
885
|
-
// and would also bloat the transcript by megabytes per image. Image
|
|
886
|
-
// bytes ride in parsedTask.images and re-enter the message tree below.
|
|
887
|
-
const taskText = parsedTask.hasImages ? parsedTask.text : task;
|
|
820
|
+
const imageInput = await prepareRunImageInput({
|
|
821
|
+
task,
|
|
822
|
+
cwd,
|
|
823
|
+
llm: this.config.llm,
|
|
824
|
+
sessionId: options?.sessionId,
|
|
825
|
+
attachments: options?.attachments,
|
|
826
|
+
});
|
|
827
|
+
if (!imageInput.ok)
|
|
828
|
+
return imageInput.result;
|
|
829
|
+
const { parsedTask, taskText } = imageInput;
|
|
888
830
|
const noise = detectPastedNoise(taskText);
|
|
889
831
|
if (noise.isNoise) {
|
|
890
832
|
const hint = `Your input looks like pasted terminal output (${noise.reason}). ` +
|
|
@@ -946,6 +888,7 @@ export class Engine {
|
|
|
946
888
|
preset: this.preset.name,
|
|
947
889
|
enabledBuiltinTools: childEnabled,
|
|
948
890
|
disabledBuiltinTools: childDisabled,
|
|
891
|
+
builtinToolHost: this.config.builtinToolHost,
|
|
949
892
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
950
893
|
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
|
|
951
894
|
undefined,
|
|
@@ -1013,23 +956,7 @@ export class Engine {
|
|
|
1013
956
|
},
|
|
1014
957
|
sessionExists: (sessionId) => this.sessionManager.exists(sessionId),
|
|
1015
958
|
};
|
|
1016
|
-
|
|
1017
|
-
// default. Read UNMERGED per-scope (getForScope) so a project that wrote no
|
|
1018
|
-
// sandbox genuinely follows global, rather than inheriting global's mode and
|
|
1019
|
-
// looking like it set one. Fixes "项目级配了不生效" + the scope model.
|
|
1020
|
-
let projectSandbox;
|
|
1021
|
-
let globalSandbox;
|
|
1022
|
-
try {
|
|
1023
|
-
const sm = this.getSettingsManager();
|
|
1024
|
-
if (this.config.isSubAgent !== true) {
|
|
1025
|
-
projectSandbox = sm.getForScope("project", cwd).sandbox;
|
|
1026
|
-
}
|
|
1027
|
-
globalSandbox = sm.getForScope("user").sandbox;
|
|
1028
|
-
}
|
|
1029
|
-
catch {
|
|
1030
|
-
// settings unavailable → fall through to per-run default
|
|
1031
|
-
}
|
|
1032
|
-
const sandboxConfig = resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
|
|
959
|
+
const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
|
|
1033
960
|
// A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
|
|
1034
961
|
// per standard §S4. resolveSandboxBackend throws when an explicit
|
|
1035
962
|
// mode is unavailable on this host; we let it propagate. The
|
|
@@ -1070,12 +997,16 @@ export class Engine {
|
|
|
1070
997
|
? sandboxBackend
|
|
1071
998
|
: { ...sandboxBackend, network: sandboxConfig.network },
|
|
1072
999
|
cwd,
|
|
1000
|
+
shellEnv: this.readShellEnv(cwd),
|
|
1073
1001
|
// TodoWrite reads this to push task_update events independently
|
|
1074
1002
|
// of its return value, so the UI's pinned task panel refreshes
|
|
1075
1003
|
// immediately rather than after the LLM next surfaces the
|
|
1076
1004
|
// snapshot. wrappedOnStream snoops the same channel to keep
|
|
1077
1005
|
// latestTodos current for TaskGuard.
|
|
1078
1006
|
streamCallback: options?.onStream,
|
|
1007
|
+
setCwd(nextCwd) {
|
|
1008
|
+
toolCtx.cwd = nextCwd;
|
|
1009
|
+
},
|
|
1079
1010
|
};
|
|
1080
1011
|
logger.info("engine.run", {
|
|
1081
1012
|
task: taskText.slice(0, 200),
|
|
@@ -1084,40 +1015,7 @@ export class Engine {
|
|
|
1084
1015
|
preset: this.preset.name,
|
|
1085
1016
|
imageCount: parsedTask.images.length,
|
|
1086
1017
|
});
|
|
1087
|
-
|
|
1088
|
-
// shape. With images, content becomes a ContentBlock[] holding one
|
|
1089
|
-
// text block (when prose is present) followed by one image block per
|
|
1090
|
-
// attachment — the provider-specific clients translate this to OpenAI
|
|
1091
|
-
// `image_url` or Anthropic `{type:image, source:base64}` downstream.
|
|
1092
|
-
// When an attached image came from a workspace FILE (the desktop composer's
|
|
1093
|
-
// path-attach flow sets ParsedImage.name = the absolute path), surface that
|
|
1094
|
-
// path to the model as text. The image bytes still ride along for vision,
|
|
1095
|
-
// but tools that operate on files — GenerateImage(referenceImages),
|
|
1096
|
-
// Read, etc. — need the on-disk path, not just the pixels. Without this the
|
|
1097
|
-
// path the composer already knew was silently dropped, and the model would
|
|
1098
|
-
// answer "图片没落到项目文件夹,找不到路径" (the seedance 图生图 dead-end).
|
|
1099
|
-
// Only names that resolve to an existing file qualify; a pasted screenshot
|
|
1100
|
-
// whose name is just "screenshot.png" is not a path and is left out.
|
|
1101
|
-
const attachedPaths = collectAttachedImagePaths(parsedTask.images, (name) => (isAbsolute(name) ? name : join(cwd, name)), existsSync);
|
|
1102
|
-
const pathHint = attachedPaths.length > 0
|
|
1103
|
-
? `\n\n<attached-image-paths>\n${attachedPaths.join("\n")}\n</attached-image-paths>\n` +
|
|
1104
|
-
`(上面附带的图片在工作区的真实路径,如需把它们作为工具输入(例如 GenerateImage 的 referenceImages、图生图参考图),直接使用这些路径。)`
|
|
1105
|
-
: "";
|
|
1106
|
-
const userMessageContent = parsedTask.hasImages
|
|
1107
|
-
? [
|
|
1108
|
-
...(parsedTask.text || pathHint
|
|
1109
|
-
? [{ type: "text", text: `${parsedTask.text}${pathHint}` }]
|
|
1110
|
-
: []),
|
|
1111
|
-
...parsedTask.images.map((img) => ({
|
|
1112
|
-
type: "image",
|
|
1113
|
-
source: {
|
|
1114
|
-
type: "base64",
|
|
1115
|
-
media_type: img.mime,
|
|
1116
|
-
data: img.base64,
|
|
1117
|
-
},
|
|
1118
|
-
})),
|
|
1119
|
-
]
|
|
1120
|
-
: taskText;
|
|
1018
|
+
const userMessageContent = buildRunUserMessageContent(parsedTask, cwd, taskText);
|
|
1121
1019
|
// Create or resume session.
|
|
1122
1020
|
//
|
|
1123
1021
|
// Three valid shapes:
|
|
@@ -1134,6 +1032,7 @@ export class Engine {
|
|
|
1134
1032
|
let session;
|
|
1135
1033
|
let messages;
|
|
1136
1034
|
let freshImageMessage;
|
|
1035
|
+
let resumedFromDisk = false;
|
|
1137
1036
|
const claimedClientMessageIds = new Set();
|
|
1138
1037
|
const claimClientMessageId = (bundle, clientMessageId, source) => {
|
|
1139
1038
|
if (!clientMessageId)
|
|
@@ -1151,6 +1050,7 @@ export class Engine {
|
|
|
1151
1050
|
return true;
|
|
1152
1051
|
};
|
|
1153
1052
|
if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
|
|
1053
|
+
resumedFromDisk = true;
|
|
1154
1054
|
session = this.sessionManager.resume(options.sessionId);
|
|
1155
1055
|
const cachedCompacted = this.compactedMessagesBySession.get(options.sessionId);
|
|
1156
1056
|
messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
|
|
@@ -1233,28 +1133,22 @@ export class Engine {
|
|
|
1233
1133
|
// with this value so `/undo` reverts exactly this turn's file changes.
|
|
1234
1134
|
// (Both resume and cold-start paths converge here.)
|
|
1235
1135
|
session.state.turnSeq = (session.state.turnSeq ?? 0) + 1;
|
|
1236
|
-
// Stamp the resolved session id for downstream logging.
|
|
1237
|
-
//
|
|
1238
|
-
// `setCurrentSid` updates the module-level fallback so any code path
|
|
1239
|
-
// running outside an ALS scope (bootstrap, /sid before a run starts)
|
|
1240
|
-
// still sees the latest sid.
|
|
1241
|
-
//
|
|
1242
|
-
// The real isolation comes from wrapping the rest of `run` in
|
|
1243
|
-
// `runWithSid(sid, async () => { ... })`: every `getCurrentSid()`
|
|
1244
|
-
// call inside that closure — including those inside `await`ed child
|
|
1245
|
-
// Engine.run() calls — reads sid from this scope's
|
|
1246
|
-
// AsyncLocalStorage binding, not the module global. Sibling Engines
|
|
1247
|
-
// running concurrently each get their own scope; an `enterSid` inside
|
|
1248
|
-
// a child mutates that child's scope only and doesn't leak back to
|
|
1249
|
-
// the parent's chain after `await child.run(...)` returns.
|
|
1250
|
-
setCurrentSid(session.state.sessionId);
|
|
1251
1136
|
// B2 / Gate 1: stamp the resolved sid onto the tool context so
|
|
1252
1137
|
// session-scoped side effects (background-agent completion
|
|
1253
1138
|
// notifications) attribute to the right session. toolCtx is created
|
|
1254
1139
|
// before the session bundle is resolved (see ~line 635), so this is
|
|
1255
1140
|
// the first point we can set it. After this assignment treat the
|
|
1256
|
-
// field
|
|
1141
|
+
// field follows the latest successfully injected user intent for the rest
|
|
1142
|
+
// of the run, so tools launched after a steer attribute their side effects
|
|
1143
|
+
// to that steer rather than this original submit.
|
|
1257
1144
|
toolCtx.sessionId = session.state.sessionId;
|
|
1145
|
+
toolCtx.originClientMessageId = options?.clientMessageId;
|
|
1146
|
+
toolCtx.recordExternalFileChanges = (record) => {
|
|
1147
|
+
session.transcript.append("external_file_changes", { ...record });
|
|
1148
|
+
};
|
|
1149
|
+
toolCtx.setSessionWorkspace = (workspace) => {
|
|
1150
|
+
session.state.workspace = workspace;
|
|
1151
|
+
};
|
|
1258
1152
|
return runWithSid(session.state.sessionId, async () => {
|
|
1259
1153
|
recordSessionStart(session.state.sessionId, {
|
|
1260
1154
|
// Strip <codeshell-image> base64 payloads before they reach
|
|
@@ -1265,7 +1159,7 @@ export class Engine {
|
|
|
1265
1159
|
model: this.config.llm.model,
|
|
1266
1160
|
provider: this.config.llm.provider,
|
|
1267
1161
|
permissionMode: this.config.permissionMode ?? "acceptEdits",
|
|
1268
|
-
resumed:
|
|
1162
|
+
resumed: resumedFromDisk,
|
|
1269
1163
|
});
|
|
1270
1164
|
// Session-level hook: fired once per Engine.run() entry, regardless of
|
|
1271
1165
|
// cold-start vs resume. Handlers can return `messages` to inject a
|
|
@@ -1275,7 +1169,8 @@ export class Engine {
|
|
|
1275
1169
|
const sessionStartHook = await this.emitHook("on_session_start", {
|
|
1276
1170
|
sessionId: session.state.sessionId,
|
|
1277
1171
|
cwd,
|
|
1278
|
-
resumed:
|
|
1172
|
+
resumed: resumedFromDisk,
|
|
1173
|
+
source: resumedFromDisk ? "resume" : "startup",
|
|
1279
1174
|
});
|
|
1280
1175
|
// Per-turn hook: fired every time a new user prompt enters the loop.
|
|
1281
1176
|
// Equivalent to CC's UserPromptSubmit. Handlers can inject lightweight
|
|
@@ -1289,7 +1184,7 @@ export class Engine {
|
|
|
1289
1184
|
// and silently leaking attachment bytes through hooks is the kind of
|
|
1290
1185
|
// exfiltration risk a curious user-installed shell hook shouldn't carry.
|
|
1291
1186
|
prompt: taskText,
|
|
1292
|
-
resumed:
|
|
1187
|
+
resumed: resumedFromDisk,
|
|
1293
1188
|
});
|
|
1294
1189
|
// updatedPrompt: handler rewrote the user's prompt text. Replace the
|
|
1295
1190
|
// last user message we just pushed (cold-start: line ~511; resume:
|
|
@@ -1307,22 +1202,49 @@ export class Engine {
|
|
|
1307
1202
|
messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
|
|
1308
1203
|
}
|
|
1309
1204
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1205
|
+
const contextManager = new ContextManager({
|
|
1206
|
+
maxTokens: this.resolveMaxContextTokens(),
|
|
1207
|
+
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1208
|
+
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1209
|
+
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1210
|
+
});
|
|
1211
|
+
this.lastContextManager = contextManager;
|
|
1212
|
+
const persistedContextAnchor = session.state.contextUsageAnchor;
|
|
1213
|
+
const contextAnchorCompatible = persistedContextAnchor !== undefined &&
|
|
1214
|
+
(persistedContextAnchor.provider === undefined ||
|
|
1215
|
+
persistedContextAnchor.provider === this.config.llm.provider) &&
|
|
1216
|
+
(persistedContextAnchor.model === undefined ||
|
|
1217
|
+
persistedContextAnchor.model === this.config.llm.model) &&
|
|
1218
|
+
(persistedContextAnchor.messageCount <= messages.length ||
|
|
1219
|
+
persistedContextAnchor.estimateAtAnchor !== undefined);
|
|
1220
|
+
if (contextAnchorCompatible) {
|
|
1221
|
+
contextManager.seedActualUsage(persistedContextAnchor);
|
|
1222
|
+
}
|
|
1223
|
+
// Best-effort token estimate of the full prompt so the UI's ctx bar isn't
|
|
1224
|
+
// 0% before the first real usage_update arrives. The authoritative count
|
|
1312
1225
|
// comes from `usage.promptTokens` after the first LLM response — this is
|
|
1313
|
-
// just a display-friendly approximation for the first frame
|
|
1226
|
+
// just a display-friendly approximation for the first frame, annotated
|
|
1227
|
+
// with source/confidence so consumers don't treat heuristics as truth.
|
|
1314
1228
|
//
|
|
1315
1229
|
// Only seed once per (process, sid). On subsequent turns the UI already
|
|
1316
|
-
// shows the previous turn's accurate ctx; overwriting it with
|
|
1317
|
-
//
|
|
1230
|
+
// shows the previous turn's accurate ctx; overwriting it with a fresh
|
|
1231
|
+
// best-effort estimate would make the bar visibly drop on every submit.
|
|
1318
1232
|
const sid = session.state.sessionId;
|
|
1319
1233
|
const needsCtxSeed = !this.ctxSeedSent.has(sid);
|
|
1320
|
-
const
|
|
1321
|
-
?
|
|
1322
|
-
const
|
|
1323
|
-
return
|
|
1324
|
-
|
|
1325
|
-
|
|
1234
|
+
const ctxSeed = needsCtxSeed
|
|
1235
|
+
? (() => {
|
|
1236
|
+
const checked = contextManager.checkLimits(messages);
|
|
1237
|
+
return {
|
|
1238
|
+
tokens: checked.tokens,
|
|
1239
|
+
source: checked.promptTokensSource,
|
|
1240
|
+
confidence: checked.promptTokensConfidence,
|
|
1241
|
+
};
|
|
1242
|
+
})()
|
|
1243
|
+
: {
|
|
1244
|
+
tokens: 0,
|
|
1245
|
+
source: "heuristic_estimate",
|
|
1246
|
+
confidence: "low",
|
|
1247
|
+
};
|
|
1326
1248
|
if (needsCtxSeed)
|
|
1327
1249
|
this.ctxSeedSent.add(sid);
|
|
1328
1250
|
// Tell the client the sid *now* instead of waiting for run() to resolve.
|
|
@@ -1331,7 +1253,9 @@ export class Engine {
|
|
|
1331
1253
|
options?.onStream?.({
|
|
1332
1254
|
type: "session_started",
|
|
1333
1255
|
sessionId: sid,
|
|
1334
|
-
promptTokens:
|
|
1256
|
+
promptTokens: ctxSeed.tokens,
|
|
1257
|
+
promptTokensSource: ctxSeed.source,
|
|
1258
|
+
promptTokensConfidence: ctxSeed.confidence,
|
|
1335
1259
|
});
|
|
1336
1260
|
// Replay the last TodoWrite snapshot on resume so the UI's pinned
|
|
1337
1261
|
// task panel re-hydrates without the LLM needing to call TodoWrite
|
|
@@ -1358,12 +1282,14 @@ export class Engine {
|
|
|
1358
1282
|
// in this same session don't re-prompt. Headless/auto backends skip
|
|
1359
1283
|
// this — they don't prompt, so there are no project rules to persist.
|
|
1360
1284
|
if (approvalBackend instanceof InteractiveApprovalBackend) {
|
|
1361
|
-
approvalBackend.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1285
|
+
approvalBackend.setSessionContext(session.state.sessionId, {
|
|
1286
|
+
cwd,
|
|
1287
|
+
onProjectRules: (rules) => {
|
|
1288
|
+
// Prepend the *full* accumulated list of session-saved project rules
|
|
1289
|
+
// so user approvals win over defaults and earlier approvals aren't
|
|
1290
|
+
// dropped when later ones come in.
|
|
1291
|
+
permission.reconfigure(mode, approvalBackend, [...rules, ...defaultRules]);
|
|
1292
|
+
},
|
|
1367
1293
|
});
|
|
1368
1294
|
}
|
|
1369
1295
|
const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
|
|
@@ -1379,13 +1305,6 @@ export class Engine {
|
|
|
1379
1305
|
// Wire abort signal for cascading cancellation + per-Engine ToolContext
|
|
1380
1306
|
toolExecutor.setSignal(options?.signal);
|
|
1381
1307
|
toolExecutor.setContext(toolCtx);
|
|
1382
|
-
const contextManager = new ContextManager({
|
|
1383
|
-
maxTokens: this.resolveMaxContextTokens(),
|
|
1384
|
-
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1385
|
-
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1386
|
-
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1387
|
-
});
|
|
1388
|
-
this.lastContextManager = contextManager;
|
|
1389
1308
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
1390
1309
|
const promptComposer = new PromptComposer({
|
|
1391
1310
|
cwd,
|
|
@@ -1447,6 +1366,7 @@ export class Engine {
|
|
|
1447
1366
|
(normalizeGoal(options?.goal) !== undefined ||
|
|
1448
1367
|
session.state.activeGoal !== undefined ||
|
|
1449
1368
|
normalizeGoal(this.config.goal) !== undefined),
|
|
1369
|
+
settingsScope: this.config.settingsScope ?? "project",
|
|
1450
1370
|
};
|
|
1451
1371
|
toolCtx.toolVisibility = toolVisibility;
|
|
1452
1372
|
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
@@ -1664,17 +1584,20 @@ export class Engine {
|
|
|
1664
1584
|
// Goal mode: register a GoalStopHook for the lifetime of THIS run so the
|
|
1665
1585
|
// turn loop keeps going until the session model judges the goal met.
|
|
1666
1586
|
// Registered per-run (and cleared in `finally`) so a later goal-less
|
|
1667
|
-
// send doesn't inherit a stale goal. The judge runs on
|
|
1668
|
-
//
|
|
1669
|
-
//
|
|
1670
|
-
// aux-tier task, and a goal run can invoke the judge up to maxStopBlocks
|
|
1671
|
-
// times.
|
|
1587
|
+
// send doesn't inherit a stale goal. The judge runs on the primary
|
|
1588
|
+
// session client; auxSummaryClient remains dedicated to low-consequence
|
|
1589
|
+
// summaries/titles and retains defaults.auxText routing/fallback behavior.
|
|
1672
1590
|
// Normalize the raw goal (string | GoalConfig) once at the run boundary;
|
|
1673
1591
|
// everything inward uses the GoalConfig. normalizeGoal() returns undefined
|
|
1674
1592
|
// when there's effectively no goal (empty objective).
|
|
1675
1593
|
//
|
|
1676
1594
|
// PERSISTENT GOAL (CC /goal style): a goal set on one send survives across
|
|
1677
|
-
// later sends and manual interrupts until met or cleared.
|
|
1595
|
+
// later sends and manual interrupts until met or cleared. Goal completion
|
|
1596
|
+
// is a high-consequence decision, so V1 routes it to the primary session
|
|
1597
|
+
// client, which is the model expected to interpret the supplied execution
|
|
1598
|
+
// evidence. defaults.auxText remains in force for summaries, titles and
|
|
1599
|
+
// other auxiliary work through auxSummaryClient.
|
|
1600
|
+
// Resolution:
|
|
1678
1601
|
// 1. options.goal — this send explicitly sets/replaces the goal.
|
|
1679
1602
|
// 2. session.state.activeGoal — a goal set on an earlier send.
|
|
1680
1603
|
// 3. config.goal — engine-level default (rare; e.g. headless).
|
|
@@ -1683,7 +1606,15 @@ export class Engine {
|
|
|
1683
1606
|
// bare send with no options.goal inherits the stored active goal so the
|
|
1684
1607
|
// model keeps working toward it — that's what makes it persistent.
|
|
1685
1608
|
const explicitGoal = normalizeGoal(options?.goal);
|
|
1686
|
-
|
|
1609
|
+
let storedGoal = this.config.isSubAgent !== true ? session.state.activeGoal : undefined;
|
|
1610
|
+
// Defense in depth: a stale whole-state writer may have restored the
|
|
1611
|
+
// activeGoal field after this exact goal instance was force-terminated.
|
|
1612
|
+
// Refuse to arm it and converge the live bundle before hook registration.
|
|
1613
|
+
if (storedGoal && isSameGoalInstance(storedGoal, session.state.goalTerminal)) {
|
|
1614
|
+
session.state.activeGoal = undefined;
|
|
1615
|
+
storedGoal = undefined;
|
|
1616
|
+
this.sessionManager.saveState(session.state);
|
|
1617
|
+
}
|
|
1687
1618
|
if (explicitGoal && this.config.isSubAgent !== true) {
|
|
1688
1619
|
const replaced = !!storedGoal && storedGoal.objective !== explicitGoal.objective;
|
|
1689
1620
|
// Stamp WHEN this goal was set so the judge can anchor relative deadlines
|
|
@@ -1692,7 +1623,14 @@ export class Engine {
|
|
|
1692
1623
|
// or changed objective gets a fresh stamp; re-sending the SAME objective
|
|
1693
1624
|
// keeps the original anchor (the goal continues, the user didn't restate a
|
|
1694
1625
|
// new deadline). User input never carries setAtMs, so we set it here.
|
|
1695
|
-
|
|
1626
|
+
const resolvedSetAt = resolveGoalSetAt(explicitGoal.objective, storedGoal, Date.now());
|
|
1627
|
+
// A user explicitly re-starting the same objective creates a new goal
|
|
1628
|
+
// instance. Avoid a same-millisecond collision with its old tombstone.
|
|
1629
|
+
explicitGoal.setAtMs =
|
|
1630
|
+
session.state.goalTerminal?.objective === explicitGoal.objective &&
|
|
1631
|
+
session.state.goalTerminal.setAtMs === resolvedSetAt
|
|
1632
|
+
? resolvedSetAt + 1
|
|
1633
|
+
: resolvedSetAt;
|
|
1696
1634
|
session.state.activeGoal = explicitGoal;
|
|
1697
1635
|
this.sessionManager.saveState(session.state);
|
|
1698
1636
|
options?.onStream?.({
|
|
@@ -1702,18 +1640,27 @@ export class Engine {
|
|
|
1702
1640
|
});
|
|
1703
1641
|
}
|
|
1704
1642
|
const normalizedGoal = explicitGoal ?? storedGoal ?? normalizeGoal(this.config.goal);
|
|
1643
|
+
// Snapshot the persisted goal identity owned by THIS run. Terminal
|
|
1644
|
+
// cleanup compares against this immutable copy so an old run cannot
|
|
1645
|
+
// delete a replacement goal installed while it was finishing.
|
|
1646
|
+
const persistedRunGoal = normalizedGoal && isSameGoalInstance(session.state.activeGoal, normalizedGoal)
|
|
1647
|
+
? { ...normalizedGoal }
|
|
1648
|
+
: undefined;
|
|
1705
1649
|
let goalHookHandler = null;
|
|
1650
|
+
let goalJudgeContext;
|
|
1706
1651
|
if (normalizedGoal && this.config.isSubAgent !== true) {
|
|
1707
1652
|
goalHookHandler = createGoalStopHook({
|
|
1708
1653
|
goal: normalizedGoal,
|
|
1709
|
-
llm:
|
|
1654
|
+
llm: llmClient,
|
|
1710
1655
|
log: logger,
|
|
1656
|
+
getJudgeContext: () => goalJudgeContext,
|
|
1711
1657
|
// Clear the persisted active goal the moment the judge says it's met,
|
|
1712
1658
|
// so a later bare send doesn't re-inherit a satisfied goal. The hook
|
|
1713
1659
|
// calls this from inside its met branch (single source of truth for
|
|
1714
1660
|
// "goal achieved"); engine owns the persistence side-effect.
|
|
1715
1661
|
onMet: () => {
|
|
1716
|
-
if (
|
|
1662
|
+
if (persistedRunGoal &&
|
|
1663
|
+
isSameGoalInstance(session.state.activeGoal, persistedRunGoal)) {
|
|
1717
1664
|
session.state.activeGoal = undefined;
|
|
1718
1665
|
this.sessionManager.saveState(session.state);
|
|
1719
1666
|
}
|
|
@@ -1723,7 +1670,7 @@ export class Engine {
|
|
|
1723
1670
|
// in-RAM session are untouched) actually stops the judge. Reads disk
|
|
1724
1671
|
// via readActiveGoal — authoritative and independent of which session
|
|
1725
1672
|
// instance the run closure holds.
|
|
1726
|
-
isGoalActive: (sid) => this.sessionManager.readActiveGoal(sid)
|
|
1673
|
+
isGoalActive: (sid) => isSameGoalInstance(this.sessionManager.readActiveGoal(sid), normalizedGoal),
|
|
1727
1674
|
});
|
|
1728
1675
|
this.hooks.register("on_stop", goalHookHandler, 0, "goal-stop");
|
|
1729
1676
|
// Expose for clearGoal() mid-run. Already guarded by isSubAgent above.
|
|
@@ -1755,14 +1702,45 @@ export class Engine {
|
|
|
1755
1702
|
return info;
|
|
1756
1703
|
},
|
|
1757
1704
|
consumeSteer: (source) => this.consumeSteer(sid, source),
|
|
1705
|
+
restoreSteer: (items) => this.restoreSteer(sid, items),
|
|
1706
|
+
buildSteerUserMessageContent: async (item) => {
|
|
1707
|
+
const steerImageInput = await prepareRunImageInput({
|
|
1708
|
+
task: item.text,
|
|
1709
|
+
cwd,
|
|
1710
|
+
llm: this.config.llm,
|
|
1711
|
+
sessionId: sid,
|
|
1712
|
+
attachments: item.attachments,
|
|
1713
|
+
});
|
|
1714
|
+
if (!steerImageInput.ok) {
|
|
1715
|
+
throw new Error(steerImageInput.result.text);
|
|
1716
|
+
}
|
|
1717
|
+
return buildRunUserMessageContent(steerImageInput.parsedTask, cwd, steerImageInput.taskText);
|
|
1718
|
+
},
|
|
1758
1719
|
claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
|
|
1720
|
+
releaseClientMessageId: (clientMessageId) => {
|
|
1721
|
+
claimedClientMessageIds.delete(clientMessageId);
|
|
1722
|
+
},
|
|
1723
|
+
setOriginClientMessageId: (clientMessageId) => {
|
|
1724
|
+
toolCtx.originClientMessageId = clientMessageId;
|
|
1725
|
+
},
|
|
1759
1726
|
recordCumulativeUsage,
|
|
1727
|
+
recordCacheReadDiagnostics: (usage) => {
|
|
1728
|
+
this.recordCacheReadDiagnostics(sid, usage);
|
|
1729
|
+
},
|
|
1730
|
+
recordContextUsageAnchor: (anchor) => {
|
|
1731
|
+
session.state.contextUsageAnchor = {
|
|
1732
|
+
...anchor,
|
|
1733
|
+
provider: this.config.llm.provider,
|
|
1734
|
+
model: this.config.llm.model,
|
|
1735
|
+
};
|
|
1736
|
+
},
|
|
1760
1737
|
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1761
1738
|
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1762
1739
|
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
1763
1740
|
// hook so nothing re-blocks the stop we're about to return.
|
|
1764
1741
|
clearPersistedGoal: () => {
|
|
1765
|
-
if (
|
|
1742
|
+
if (persistedRunGoal &&
|
|
1743
|
+
isSameGoalInstance(session.state.activeGoal, persistedRunGoal)) {
|
|
1766
1744
|
session.state.activeGoal = undefined;
|
|
1767
1745
|
this.sessionManager.saveState(session.state);
|
|
1768
1746
|
}
|
|
@@ -1772,6 +1750,9 @@ export class Engine {
|
|
|
1772
1750
|
this.activeGoalHook = null;
|
|
1773
1751
|
}
|
|
1774
1752
|
},
|
|
1753
|
+
updateGoalJudgeContext: (context) => {
|
|
1754
|
+
goalJudgeContext = context;
|
|
1755
|
+
},
|
|
1775
1756
|
ctxOverheadStore: {
|
|
1776
1757
|
get: (s) => this.ctxOverheadBySid.get(s) ?? 0,
|
|
1777
1758
|
set: (s, n) => {
|
|
@@ -1796,6 +1777,7 @@ export class Engine {
|
|
|
1796
1777
|
onStream: options?.onStream,
|
|
1797
1778
|
signal: options?.signal,
|
|
1798
1779
|
freshImageMessages: freshImageMessage ? [freshImageMessage] : undefined,
|
|
1780
|
+
volatileContextMessages: dynamicContextMsg ? [dynamicContextMsg] : undefined,
|
|
1799
1781
|
// Goal mode: the active goal is surfaced to the on_stop handler via
|
|
1800
1782
|
// ctx.data.goal; the GoalStopHook (registered above) judges it.
|
|
1801
1783
|
goal: normalizedGoal,
|
|
@@ -1816,6 +1798,8 @@ export class Engine {
|
|
|
1816
1798
|
options?.onStream?.({
|
|
1817
1799
|
type: "usage_update",
|
|
1818
1800
|
promptTokens: cumulative.cumulativePromptTokens,
|
|
1801
|
+
promptTokensSource: "session_cumulative",
|
|
1802
|
+
promptTokensConfidence: "high",
|
|
1819
1803
|
cumulativePromptTokens: cumulative.cumulativePromptTokens,
|
|
1820
1804
|
cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
1821
1805
|
cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
@@ -1841,9 +1825,31 @@ export class Engine {
|
|
|
1841
1825
|
// only — sub-agents don't carry user-clearable persistent goals.
|
|
1842
1826
|
if (this.config.isSubAgent !== true)
|
|
1843
1827
|
this.activeRunSession = session;
|
|
1828
|
+
const applyGoalTermination = (termination) => {
|
|
1829
|
+
if (!termination || !persistedRunGoal)
|
|
1830
|
+
return;
|
|
1831
|
+
// Record the terminal identity even when a newer goal has already
|
|
1832
|
+
// replaced it. Only clear activeGoal when it is still the run's goal.
|
|
1833
|
+
session.state.goalTerminal = {
|
|
1834
|
+
objective: persistedRunGoal.objective,
|
|
1835
|
+
setAtMs: persistedRunGoal.setAtMs,
|
|
1836
|
+
reason: termination,
|
|
1837
|
+
terminatedAtMs: Date.now(),
|
|
1838
|
+
};
|
|
1839
|
+
if (isSameGoalInstance(session.state.activeGoal, persistedRunGoal)) {
|
|
1840
|
+
session.state.activeGoal = undefined;
|
|
1841
|
+
}
|
|
1842
|
+
this.sessionManager.saveState(session.state);
|
|
1843
|
+
if (goalHookHandler) {
|
|
1844
|
+
this.hooks.unregister("on_stop", goalHookHandler);
|
|
1845
|
+
if (this.activeGoalHook === goalHookHandler)
|
|
1846
|
+
this.activeGoalHook = null;
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1844
1849
|
let result;
|
|
1845
1850
|
try {
|
|
1846
1851
|
result = await turnLoop.run(messages);
|
|
1852
|
+
applyGoalTermination(result.goalTermination);
|
|
1847
1853
|
// ── Headless: drain background sub-agents before resolving ───────
|
|
1848
1854
|
// Unified background-work model (2026-06-17): the engine NO LONGER parks
|
|
1849
1855
|
// every run waiting on background work. Background work (sub-agents,
|
|
@@ -1905,6 +1911,7 @@ export class Engine {
|
|
|
1905
1911
|
break;
|
|
1906
1912
|
}
|
|
1907
1913
|
result = await turnLoop.run([...result.messages, injected]);
|
|
1914
|
+
applyGoalTermination(result.goalTermination);
|
|
1908
1915
|
}
|
|
1909
1916
|
}
|
|
1910
1917
|
}
|
|
@@ -1924,7 +1931,8 @@ export class Engine {
|
|
|
1924
1931
|
this.hooks.unregister("on_tool_start", fileHistoryHandler);
|
|
1925
1932
|
}
|
|
1926
1933
|
this.lastMessages = result.messages;
|
|
1927
|
-
|
|
1934
|
+
const cachedMessages = this.stripInjectedContextMessages(result.messages, userContextMsg, dynamicContextMsg);
|
|
1935
|
+
this.compactedMessagesBySession.set(session.state.sessionId, cachedMessages);
|
|
1928
1936
|
logger.info("engine.done", {
|
|
1929
1937
|
sessionId: session.state.sessionId,
|
|
1930
1938
|
reason: result.reason,
|
|
@@ -2355,7 +2363,6 @@ export class Engine {
|
|
|
2355
2363
|
refreshRuntimeConfig(patch, version) {
|
|
2356
2364
|
if (version <= this.lastAppliedConfigVersion)
|
|
2357
2365
|
return;
|
|
2358
|
-
const prevServers = this.config.mcpServers ?? {};
|
|
2359
2366
|
const prevPresetName = this.preset.name;
|
|
2360
2367
|
this.config = { ...this.config, ...patch };
|
|
2361
2368
|
// #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
|
|
@@ -2366,11 +2373,17 @@ export class Engine {
|
|
|
2366
2373
|
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
2367
2374
|
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
2368
2375
|
// tool set, that part of the change only lands on session restart.
|
|
2369
|
-
const prevTools = resolveBuiltinToolNames({
|
|
2376
|
+
const prevTools = resolveBuiltinToolNames({
|
|
2377
|
+
preset: prevPresetName,
|
|
2378
|
+
host: this.config.builtinToolHost,
|
|
2379
|
+
})
|
|
2370
2380
|
.slice()
|
|
2371
2381
|
.sort()
|
|
2372
2382
|
.join(",");
|
|
2373
|
-
const nextTools = resolveBuiltinToolNames({
|
|
2383
|
+
const nextTools = resolveBuiltinToolNames({
|
|
2384
|
+
preset: nextPreset.name,
|
|
2385
|
+
host: this.config.builtinToolHost,
|
|
2386
|
+
})
|
|
2374
2387
|
.slice()
|
|
2375
2388
|
.sort()
|
|
2376
2389
|
.join(",");
|
|
@@ -2457,6 +2470,41 @@ export class Engine {
|
|
|
2457
2470
|
}
|
|
2458
2471
|
return had;
|
|
2459
2472
|
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Reset a session's workspace pointer back to its main root. If the session is
|
|
2475
|
+
* actively running, mutate that live SessionBundle first so the run's next
|
|
2476
|
+
* saveState cannot resurrect a stale worktree pointer.
|
|
2477
|
+
*/
|
|
2478
|
+
releaseSessionWorkspace(sessionId) {
|
|
2479
|
+
if (!sessionId || !this.sessionManager.exists(sessionId))
|
|
2480
|
+
return null;
|
|
2481
|
+
const mainRoot = this.sessionManager.readCwd(sessionId) ??
|
|
2482
|
+
(this.activeRunSession?.state.sessionId === sessionId
|
|
2483
|
+
? this.activeRunSession.state.cwd
|
|
2484
|
+
: undefined);
|
|
2485
|
+
if (!mainRoot)
|
|
2486
|
+
return null;
|
|
2487
|
+
const workspace = { root: mainRoot, kind: "main" };
|
|
2488
|
+
if (this.activeRunSession?.state.sessionId === sessionId) {
|
|
2489
|
+
this.activeRunSession.state.workspace = workspace;
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const bundle = this.activeRunSession?.state.sessionId === sessionId
|
|
2493
|
+
? this.activeRunSession
|
|
2494
|
+
: this.sessionManager.resume(sessionId);
|
|
2495
|
+
bundle.state.workspace = workspace;
|
|
2496
|
+
this.sessionManager.saveState(bundle.state);
|
|
2497
|
+
}
|
|
2498
|
+
catch {
|
|
2499
|
+
try {
|
|
2500
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace);
|
|
2501
|
+
}
|
|
2502
|
+
catch {
|
|
2503
|
+
return null;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return workspace;
|
|
2507
|
+
}
|
|
2460
2508
|
injectContext(sessionId, content) {
|
|
2461
2509
|
const session = this.sessionManager.resume(sessionId);
|
|
2462
2510
|
session.transcript.appendMessage("assistant", content);
|
|
@@ -2539,11 +2587,39 @@ export class Engine {
|
|
|
2539
2587
|
strategy: after >= before ? "no compaction needed" : (compactStrategy ?? "compacted"),
|
|
2540
2588
|
};
|
|
2541
2589
|
}
|
|
2542
|
-
|
|
2590
|
+
stripInjectedContextMessages(messages, userContextMsg, dynamicContextMsg) {
|
|
2591
|
+
const withoutDynamicContext = dynamicContextMsg
|
|
2592
|
+
? messages.filter((msg) => msg !== dynamicContextMsg)
|
|
2593
|
+
: [...messages];
|
|
2543
2594
|
if (!userContextMsg || messages[0] !== userContextMsg) {
|
|
2544
|
-
return
|
|
2595
|
+
return withoutDynamicContext;
|
|
2596
|
+
}
|
|
2597
|
+
return withoutDynamicContext.slice(1);
|
|
2598
|
+
}
|
|
2599
|
+
recordCacheReadDiagnostics(sessionId, usage) {
|
|
2600
|
+
const current = usage.cacheReadTokens;
|
|
2601
|
+
if (current === undefined || !Number.isFinite(current))
|
|
2602
|
+
return;
|
|
2603
|
+
const previous = this.lastCacheReadBySid.get(sessionId);
|
|
2604
|
+
this.lastCacheReadBySid.delete(sessionId);
|
|
2605
|
+
this.lastCacheReadBySid.set(sessionId, current);
|
|
2606
|
+
if (this.lastCacheReadBySid.size > CACHE_READ_DIAGNOSTIC_MAX_SESSIONS) {
|
|
2607
|
+
const oldestSessionId = this.lastCacheReadBySid.keys().next().value;
|
|
2608
|
+
if (oldestSessionId !== undefined)
|
|
2609
|
+
this.lastCacheReadBySid.delete(oldestSessionId);
|
|
2610
|
+
}
|
|
2611
|
+
if (previous === undefined || previous < CACHE_READ_DROP_MIN_PREVIOUS_TOKENS)
|
|
2612
|
+
return;
|
|
2613
|
+
const dropRatio = previous > 0 ? current / previous : 1;
|
|
2614
|
+
if (current <= CACHE_READ_DROP_MAX_CURRENT_TOKENS && dropRatio <= CACHE_READ_DROP_RATIO) {
|
|
2615
|
+
logger.warn("engine.cache_read_drop", {
|
|
2616
|
+
sessionId,
|
|
2617
|
+
previousCacheReadTokens: previous,
|
|
2618
|
+
currentCacheReadTokens: current,
|
|
2619
|
+
dropRatio,
|
|
2620
|
+
hint: "Prompt cache read tokens dropped sharply. Check for changed cacheable prefix, stale dynamic context in history, tool/schema changes, or provider cache eviction.",
|
|
2621
|
+
});
|
|
2545
2622
|
}
|
|
2546
|
-
return messages.slice(1);
|
|
2547
2623
|
}
|
|
2548
2624
|
getSettingsManager() {
|
|
2549
2625
|
if (!this.settingsManager) {
|
|
@@ -2575,10 +2651,10 @@ export class Engine {
|
|
|
2575
2651
|
buildPermissionConfig(mode, cwd) {
|
|
2576
2652
|
const rules = [...this.preset.defaultPermissionRules];
|
|
2577
2653
|
// Memory tools: dream scope is the LLM's own workspace, so save/delete
|
|
2578
|
-
// there go through without prompting. user-scope save/delete
|
|
2579
|
-
//
|
|
2580
|
-
//
|
|
2581
|
-
//
|
|
2654
|
+
// there go through without prompting. user-scope save/delete have no
|
|
2655
|
+
// explicit allow rule here, so default-mode classifier fallback asks the
|
|
2656
|
+
// user to confirm modifications. RegisteredTool.permissionDefault is only
|
|
2657
|
+
// UI/metadata and is not read by the classifier.
|
|
2582
2658
|
rules.push({
|
|
2583
2659
|
tool: "MemorySave",
|
|
2584
2660
|
argsPattern: { scope: "^dream$" },
|
|
@@ -2837,6 +2913,25 @@ export class Engine {
|
|
|
2837
2913
|
}
|
|
2838
2914
|
return cached;
|
|
2839
2915
|
}
|
|
2916
|
+
resolveSandboxConfigForCwd(cwd) {
|
|
2917
|
+
// Priority: config.sandbox → project settings.sandbox → global → per-run
|
|
2918
|
+
// default. Read UNMERGED per-scope (getForScope) so a project that wrote no
|
|
2919
|
+
// sandbox genuinely follows global, rather than inheriting global's mode and
|
|
2920
|
+
// looking like it set one. Fixes "项目级配了不生效" + the scope model.
|
|
2921
|
+
let projectSandbox;
|
|
2922
|
+
let globalSandbox;
|
|
2923
|
+
try {
|
|
2924
|
+
const sm = this.getSettingsManager();
|
|
2925
|
+
if (this.config.isSubAgent !== true) {
|
|
2926
|
+
projectSandbox = sm.getForScope("project", cwd).sandbox;
|
|
2927
|
+
}
|
|
2928
|
+
globalSandbox = sm.getForScope("user").sandbox;
|
|
2929
|
+
}
|
|
2930
|
+
catch {
|
|
2931
|
+
// settings unavailable → fall through to per-run default
|
|
2932
|
+
}
|
|
2933
|
+
return resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
|
|
2934
|
+
}
|
|
2840
2935
|
/**
|
|
2841
2936
|
* Build the shell env layered onto the Bash tool / background shells (see
|
|
2842
2937
|
* mergeShellEnv). Three user-configured sources, merged lowest → highest:
|
|
@@ -2894,7 +2989,7 @@ export class Engine {
|
|
|
2894
2989
|
// the host user's credentials (same isolation contract as top-level env).
|
|
2895
2990
|
// Placed below settings.env so an explicit `env` entry can still override.
|
|
2896
2991
|
const credScope = (this.config.settingsScope ?? "project") === "full" ? "full" : "project";
|
|
2897
|
-
layer(
|
|
2992
|
+
layer(getCredentialAccess().envExposures(cwd, credScope));
|
|
2898
2993
|
layer(settings.env); // top-level env (global ⊕ project) wins
|
|
2899
2994
|
}
|
|
2900
2995
|
catch {
|
|
@@ -2931,9 +3026,34 @@ export class Engine {
|
|
|
2931
3026
|
return undefined;
|
|
2932
3027
|
}
|
|
2933
3028
|
}
|
|
3029
|
+
readWorktreeBranchPrefix(cwd) {
|
|
3030
|
+
if (this.config.isSubAgent === true || !cwd)
|
|
3031
|
+
return undefined;
|
|
3032
|
+
try {
|
|
3033
|
+
const settings = this.getSettingsManager().get();
|
|
3034
|
+
return settings.worktree?.branchPrefix;
|
|
3035
|
+
}
|
|
3036
|
+
catch {
|
|
3037
|
+
return undefined;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
async resolveWorktreeSetupSandbox(cwd) {
|
|
3041
|
+
if (!cwd)
|
|
3042
|
+
return undefined;
|
|
3043
|
+
const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
|
|
3044
|
+
const sandboxBackend = this.runtime
|
|
3045
|
+
? await this.runtime.resolveSandbox(sandboxConfig, cwd)
|
|
3046
|
+
: await this.resolveSandboxWithoutRuntime(sandboxConfig, cwd);
|
|
3047
|
+
return sandboxBackend.name === "off"
|
|
3048
|
+
? sandboxBackend
|
|
3049
|
+
: { ...sandboxBackend, network: sandboxConfig.network };
|
|
3050
|
+
}
|
|
3051
|
+
readWorktreeSetupShellEnv(cwd) {
|
|
3052
|
+
return this.readShellEnv(cwd);
|
|
3053
|
+
}
|
|
2934
3054
|
buildToolContext() {
|
|
2935
3055
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
2936
|
-
|
|
3056
|
+
const ctx = {
|
|
2937
3057
|
shellEnv: this.readShellEnv(this.config.cwd),
|
|
2938
3058
|
cwd: this.config.cwd ?? process.cwd(),
|
|
2939
3059
|
llmConfig: this.config.llm,
|
|
@@ -2941,6 +3061,7 @@ export class Engine {
|
|
|
2941
3061
|
toolRegistry: this.toolRegistry,
|
|
2942
3062
|
askUser: this.config.askUser,
|
|
2943
3063
|
browser: this.config.browserBridge,
|
|
3064
|
+
workspace: this.config.workspaceBridge,
|
|
2944
3065
|
injectCredentialToBrowser: this.config.injectCredentialToBrowser,
|
|
2945
3066
|
isSubAgent: this.config.isSubAgent === true,
|
|
2946
3067
|
// Credential tools narrow their disk reads to this scope: a project/
|
|
@@ -2961,6 +3082,10 @@ export class Engine {
|
|
|
2961
3082
|
// opts out via config. Otherwise allowed.
|
|
2962
3083
|
allowBackgroundShells: this.config.isSubAgent === true ? false : this.config.allowBackgroundShells !== false,
|
|
2963
3084
|
};
|
|
3085
|
+
ctx.setCwd = (cwd) => {
|
|
3086
|
+
ctx.cwd = cwd;
|
|
3087
|
+
};
|
|
3088
|
+
return ctx;
|
|
2964
3089
|
}
|
|
2965
3090
|
/**
|
|
2966
3091
|
* Read settings.disabledSkills + settings.disabledPlugins in a single
|