@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.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/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 +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- 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-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- 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.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- 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 +38 -12
- package/dist/engine/engine.js +477 -163
- 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 +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -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 +34 -2
- package/dist/engine/turn-loop.js +269 -52
- 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/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 +16 -9
- 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 +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -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/safe-spawn.js +74 -11
- 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 +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -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/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- 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/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- 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 +37 -21
- 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/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 +23 -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 +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- 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 +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
package/dist/engine/engine.js
CHANGED
|
@@ -11,7 +11,7 @@ import { applyDynamicToolDef } from "./dynamic-tool-defs.js";
|
|
|
11
11
|
import { getMergedCatalog } from "../model-catalog/index.js";
|
|
12
12
|
import { modelEntriesFromConnections } from "./model-connections-pool.js";
|
|
13
13
|
import { resolveAuxKey } from "./aux-key.js";
|
|
14
|
-
import { foldRunUsage } from "./session-usage.js";
|
|
14
|
+
import { addCumulativeUsage, cumulativeCacheHitRate, foldRunUsage, normalizeCumulativeUsageCounters, } from "./session-usage.js";
|
|
15
15
|
import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-queue.js";
|
|
16
16
|
import { resolveSandboxConfig } from "./sandbox-config.js";
|
|
17
17
|
import { sandboxCacheKey } from "./sandbox-cache-key.js";
|
|
@@ -34,26 +34,27 @@ 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
|
-
import { detectProviderFromApiKey, buildModelPool
|
|
54
|
+
import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
|
|
55
55
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
56
|
-
import { parseTaskWithImages
|
|
56
|
+
import { parseTaskWithImages } from "./parse-task.js";
|
|
57
|
+
import { buildInputAttachmentContext } from "./input-attachments.js";
|
|
57
58
|
import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, collectAttachedImagePaths, } from "./image-policy.js";
|
|
58
59
|
import { tryCompressImages } from "./image-compression.js";
|
|
59
60
|
import { buildSessionTitle } from "./session-title.js";
|
|
@@ -61,7 +62,7 @@ import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
|
61
62
|
import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
62
63
|
import { runDreamConsolidation } from "../services/dream-consolidation.js";
|
|
63
64
|
import { join, isAbsolute } from "node:path";
|
|
64
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync
|
|
65
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
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
|
|
@@ -440,13 +441,16 @@ export class Engine {
|
|
|
440
441
|
// per-turn path can only hide, not add, so a freshly-`on`'d builtin not in
|
|
441
442
|
// the set needs a session restart to appear.
|
|
442
443
|
const builtinLists = effectiveBuiltinLists(config.enabledBuiltinTools ?? [], config.disabledBuiltinTools ?? [], this.readBuiltinOverride(config.cwd));
|
|
443
|
-
this.toolRegistry =
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
444
|
+
this.toolRegistry =
|
|
445
|
+
config.runtime?.toolRegistry ??
|
|
446
|
+
new ToolRegistry({
|
|
447
|
+
builtinTools: resolveBuiltinToolNames({
|
|
448
|
+
preset: this.preset.name,
|
|
449
|
+
host: config.builtinToolHost,
|
|
450
|
+
enabledBuiltinTools: builtinLists.enabledBuiltinTools,
|
|
451
|
+
disabledBuiltinTools: builtinLists.disabledBuiltinTools,
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
450
454
|
this.hooks = new HookRegistry();
|
|
451
455
|
// Installed-plugin hooks — declared in each plugin's hooks/hooks.json.
|
|
452
456
|
// Registered first (priority 80) so user-authored hooks at lower
|
|
@@ -633,27 +637,52 @@ export class Engine {
|
|
|
633
637
|
setBrowserBridge(bridge) {
|
|
634
638
|
this.config.browserBridge = bridge;
|
|
635
639
|
}
|
|
640
|
+
/** Inject the host-backed workspace bridge after construction. */
|
|
641
|
+
setWorkspaceBridge(bridge) {
|
|
642
|
+
this.config.workspaceBridge = bridge;
|
|
643
|
+
}
|
|
636
644
|
/**
|
|
637
645
|
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
638
646
|
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
639
647
|
* resend). General-purpose: any host path (UI 引导, future agent coordination,
|
|
640
|
-
* external triggers) can call it. If no run is active for
|
|
641
|
-
*
|
|
642
|
-
*
|
|
648
|
+
* external triggers) can call it. If no run is active for this session, reject
|
|
649
|
+
* without queueing so the host can downgrade to a normal run immediately.
|
|
650
|
+
* No-op on blank text.
|
|
643
651
|
*
|
|
644
652
|
* `id` is the host's stable queue-entry id. It rides through to the
|
|
645
653
|
* `steer_injected` event (so the host can match the injected bubble back to
|
|
646
654
|
* the queued draft) and is the handle `unsteer` uses to revoke a still-pending
|
|
647
655
|
* entry. A blank id is tolerated but means the entry can't be revoked.
|
|
648
656
|
*/
|
|
649
|
-
enqueueSteer(sessionId, text, id = "") {
|
|
650
|
-
if (!sessionId)
|
|
651
|
-
return;
|
|
657
|
+
enqueueSteer(sessionId, text, id = "", clientMessageId) {
|
|
652
658
|
const q = this.steerQueueBySid.get(sessionId) ?? [];
|
|
653
|
-
const
|
|
659
|
+
const entryId = id || `steer-${q.length}`;
|
|
660
|
+
if (!sessionId)
|
|
661
|
+
return { accepted: false, id: entryId };
|
|
662
|
+
const activeRunSessionId = this.activeRunSession?.state.sessionId;
|
|
663
|
+
const active = this.activeTurnLoop !== null && activeRunSessionId === sessionId;
|
|
664
|
+
if (!active) {
|
|
665
|
+
logger.info("steer.enqueue.idle_rejected", {
|
|
666
|
+
sessionId,
|
|
667
|
+
id: entryId,
|
|
668
|
+
clientMessageId,
|
|
669
|
+
activeRunSessionId: activeRunSessionId ?? null,
|
|
670
|
+
queueLength: q.length,
|
|
671
|
+
});
|
|
672
|
+
return { accepted: false, id: entryId };
|
|
673
|
+
}
|
|
674
|
+
const next = enqueueSteerItem(q, entryId, text, clientMessageId);
|
|
654
675
|
if (next === q)
|
|
655
|
-
return; // blank text dropped
|
|
676
|
+
return { accepted: false, id: entryId }; // blank text dropped
|
|
656
677
|
this.steerQueueBySid.set(sessionId, next);
|
|
678
|
+
logger.info("steer.enqueue.accepted", {
|
|
679
|
+
sessionId,
|
|
680
|
+
id: entryId,
|
|
681
|
+
clientMessageId,
|
|
682
|
+
activeRunSessionId,
|
|
683
|
+
queueLength: next.length,
|
|
684
|
+
});
|
|
685
|
+
return { accepted: true, id: entryId };
|
|
657
686
|
}
|
|
658
687
|
/**
|
|
659
688
|
* Revoke a still-pending steer entry (the 撤回 path). Returns true if it was
|
|
@@ -670,12 +699,20 @@ export class Engine {
|
|
|
670
699
|
return removed;
|
|
671
700
|
}
|
|
672
701
|
/** Drain + clear the steer queue for a session (turn loop consumes per step). */
|
|
673
|
-
consumeSteer(sessionId) {
|
|
702
|
+
consumeSteer(sessionId, source = "normal_step") {
|
|
674
703
|
const q = this.steerQueueBySid.get(sessionId);
|
|
675
704
|
if (!q || q.length === 0)
|
|
676
705
|
return [];
|
|
677
706
|
const { drained, rest } = consumeSteerItems(q);
|
|
678
707
|
this.steerQueueBySid.set(sessionId, rest);
|
|
708
|
+
logger.info("steer.consume.drained", {
|
|
709
|
+
sessionId,
|
|
710
|
+
source,
|
|
711
|
+
count: drained.length,
|
|
712
|
+
ids: drained.map((item) => item.id),
|
|
713
|
+
clientMessageIds: drained.flatMap((item) => item.clientMessageId ? [item.clientMessageId] : []),
|
|
714
|
+
queueLength: rest.length,
|
|
715
|
+
});
|
|
679
716
|
return drained;
|
|
680
717
|
}
|
|
681
718
|
/** Wire the cookie→browser injection callback (InjectCredential tool). Same
|
|
@@ -704,20 +741,45 @@ export class Engine {
|
|
|
704
741
|
* Run a task from start to finish.
|
|
705
742
|
*/
|
|
706
743
|
async run(task, options) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
// loading its own agents/settings/memory even if the host's UI repo
|
|
710
|
-
// selection has drifted to null. Only probe on omission — an explicit cwd
|
|
711
|
-
// always wins, and a fresh session has nothing to recover.
|
|
712
|
-
const sessionCwd = options?.cwd === undefined && options?.sessionId
|
|
713
|
-
? this.sessionManager.readCwd(options.sessionId)
|
|
744
|
+
const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
|
|
745
|
+
? await this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
714
746
|
: undefined;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
747
|
+
if (workspaceResume && !workspaceResume.ok) {
|
|
748
|
+
return {
|
|
749
|
+
text: `ERROR: ${workspaceResume.message}`,
|
|
750
|
+
reason: "completed",
|
|
751
|
+
sessionId: options.sessionId,
|
|
752
|
+
turnCount: 0,
|
|
753
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (workspaceResume?.ok &&
|
|
757
|
+
workspaceResume.reason === "worktree_missing_branch_gone" &&
|
|
758
|
+
workspaceResume.message) {
|
|
759
|
+
return {
|
|
760
|
+
text: workspaceResume.message,
|
|
761
|
+
reason: "completed",
|
|
762
|
+
sessionId: options.sessionId,
|
|
763
|
+
turnCount: 0,
|
|
764
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
// Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
|
|
768
|
+
// passes a stale cwd. Legacy sessions without workspace keep the historical
|
|
769
|
+
// explicit-cwd precedence for backward compatibility.
|
|
770
|
+
const workspaceCwd = workspaceResume?.ok && workspaceResume.reason !== "legacy" ? workspaceResume.cwd : undefined;
|
|
771
|
+
const sessionCwd = workspaceCwd === undefined && options?.cwd === undefined && options?.sessionId
|
|
772
|
+
? workspaceResume?.ok
|
|
773
|
+
? workspaceResume.cwd
|
|
774
|
+
: this.sessionManager.readCwd(options.sessionId)
|
|
775
|
+
: undefined;
|
|
776
|
+
const cwd = workspaceCwd ??
|
|
777
|
+
resolveRunCwd({
|
|
778
|
+
optionCwd: options?.cwd,
|
|
779
|
+
sessionCwd,
|
|
780
|
+
configCwd: this.config.cwd,
|
|
781
|
+
processCwd: process.cwd(),
|
|
782
|
+
});
|
|
721
783
|
// Wrap the caller's onStream so we can intercept `task_update`
|
|
722
784
|
// events emitted by TodoWrite and keep an in-engine snapshot.
|
|
723
785
|
// TaskGuard reads this snapshot at turn end to decide whether to
|
|
@@ -770,8 +832,32 @@ export class Engine {
|
|
|
770
832
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
771
833
|
};
|
|
772
834
|
}
|
|
835
|
+
const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
|
|
836
|
+
const attachmentContext = await buildInputAttachmentContext(options?.attachments, cwd, {
|
|
837
|
+
includeImageBytes: cap.supportsVision,
|
|
838
|
+
expectedSessionId: options?.sessionId,
|
|
839
|
+
});
|
|
840
|
+
if (attachmentContext.errors.length > 0) {
|
|
841
|
+
const detail = attachmentContext.errors.join("; ");
|
|
842
|
+
logger.warn("engine.run.input_attachment_failed", { error: detail });
|
|
843
|
+
return {
|
|
844
|
+
text: `ERROR: input attachment could not be read (${detail}). Re-attach it or choose a path inside the workspace.`,
|
|
845
|
+
reason: "image_error",
|
|
846
|
+
sessionId: options?.sessionId ?? "input-attachment-failed",
|
|
847
|
+
turnCount: 0,
|
|
848
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
if (attachmentContext.text || attachmentContext.hasStructuredImageAttachments) {
|
|
852
|
+
parsedTask = {
|
|
853
|
+
text: [parsedTask.text, attachmentContext.text].filter(Boolean).join("\n\n"),
|
|
854
|
+
images: [...parsedTask.images, ...attachmentContext.images],
|
|
855
|
+
hasImages: parsedTask.hasImages ||
|
|
856
|
+
attachmentContext.images.length > 0 ||
|
|
857
|
+
attachmentContext.hasStructuredImageAttachments,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
773
860
|
if (parsedTask.hasImages) {
|
|
774
|
-
const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
|
|
775
861
|
if (!cap.supportsVision) {
|
|
776
862
|
logger.warn("engine.run.vision_not_supported", {
|
|
777
863
|
provider: this.config.llm.provider,
|
|
@@ -853,7 +939,7 @@ export class Engine {
|
|
|
853
939
|
// *text* portion only — base64 bytes count as "noise" by the heuristic
|
|
854
940
|
// and would also bloat the transcript by megabytes per image. Image
|
|
855
941
|
// bytes ride in parsedTask.images and re-enter the message tree below.
|
|
856
|
-
const taskText = parsedTask.
|
|
942
|
+
const taskText = parsedTask.text;
|
|
857
943
|
const noise = detectPastedNoise(taskText);
|
|
858
944
|
if (noise.isNoise) {
|
|
859
945
|
const hint = `Your input looks like pasted terminal output (${noise.reason}). ` +
|
|
@@ -915,10 +1001,10 @@ export class Engine {
|
|
|
915
1001
|
preset: this.preset.name,
|
|
916
1002
|
enabledBuiltinTools: childEnabled,
|
|
917
1003
|
disabledBuiltinTools: childDisabled,
|
|
1004
|
+
builtinToolHost: this.config.builtinToolHost,
|
|
918
1005
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
919
|
-
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt]
|
|
920
|
-
|
|
921
|
-
.join("\n\n") || undefined,
|
|
1006
|
+
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
|
|
1007
|
+
undefined,
|
|
922
1008
|
responseLanguage: this.config.responseLanguage,
|
|
923
1009
|
userProfile: this.config.userProfile,
|
|
924
1010
|
instructions: this.config.instructions,
|
|
@@ -983,23 +1069,7 @@ export class Engine {
|
|
|
983
1069
|
},
|
|
984
1070
|
sessionExists: (sessionId) => this.sessionManager.exists(sessionId),
|
|
985
1071
|
};
|
|
986
|
-
|
|
987
|
-
// default. Read UNMERGED per-scope (getForScope) so a project that wrote no
|
|
988
|
-
// sandbox genuinely follows global, rather than inheriting global's mode and
|
|
989
|
-
// looking like it set one. Fixes "项目级配了不生效" + the scope model.
|
|
990
|
-
let projectSandbox;
|
|
991
|
-
let globalSandbox;
|
|
992
|
-
try {
|
|
993
|
-
const sm = this.getSettingsManager();
|
|
994
|
-
if (this.config.isSubAgent !== true) {
|
|
995
|
-
projectSandbox = sm.getForScope("project", cwd).sandbox;
|
|
996
|
-
}
|
|
997
|
-
globalSandbox = sm.getForScope("user").sandbox;
|
|
998
|
-
}
|
|
999
|
-
catch {
|
|
1000
|
-
// settings unavailable → fall through to per-run default
|
|
1001
|
-
}
|
|
1002
|
-
const sandboxConfig = resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
|
|
1072
|
+
const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
|
|
1003
1073
|
// A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
|
|
1004
1074
|
// per standard §S4. resolveSandboxBackend throws when an explicit
|
|
1005
1075
|
// mode is unavailable on this host; we let it propagate. The
|
|
@@ -1040,12 +1110,16 @@ export class Engine {
|
|
|
1040
1110
|
? sandboxBackend
|
|
1041
1111
|
: { ...sandboxBackend, network: sandboxConfig.network },
|
|
1042
1112
|
cwd,
|
|
1113
|
+
shellEnv: this.readShellEnv(cwd),
|
|
1043
1114
|
// TodoWrite reads this to push task_update events independently
|
|
1044
1115
|
// of its return value, so the UI's pinned task panel refreshes
|
|
1045
1116
|
// immediately rather than after the LLM next surfaces the
|
|
1046
1117
|
// snapshot. wrappedOnStream snoops the same channel to keep
|
|
1047
1118
|
// latestTodos current for TaskGuard.
|
|
1048
1119
|
streamCallback: options?.onStream,
|
|
1120
|
+
setCwd(nextCwd) {
|
|
1121
|
+
toolCtx.cwd = nextCwd;
|
|
1122
|
+
},
|
|
1049
1123
|
};
|
|
1050
1124
|
logger.info("engine.run", {
|
|
1051
1125
|
task: taskText.slice(0, 200),
|
|
@@ -1103,7 +1177,26 @@ export class Engine {
|
|
|
1103
1177
|
// call) instead of a try/catch on resume.
|
|
1104
1178
|
let session;
|
|
1105
1179
|
let messages;
|
|
1180
|
+
let freshImageMessage;
|
|
1181
|
+
let resumedFromDisk = false;
|
|
1182
|
+
const claimedClientMessageIds = new Set();
|
|
1183
|
+
const claimClientMessageId = (bundle, clientMessageId, source) => {
|
|
1184
|
+
if (!clientMessageId)
|
|
1185
|
+
return true;
|
|
1186
|
+
if (claimedClientMessageIds.has(clientMessageId) ||
|
|
1187
|
+
bundle.transcript.hasClientMessageId(clientMessageId)) {
|
|
1188
|
+
logger.info("engine.client_message.duplicate_ignored", {
|
|
1189
|
+
sessionId: bundle.state.sessionId,
|
|
1190
|
+
clientMessageId,
|
|
1191
|
+
source,
|
|
1192
|
+
});
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
claimedClientMessageIds.add(clientMessageId);
|
|
1196
|
+
return true;
|
|
1197
|
+
};
|
|
1106
1198
|
if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
|
|
1199
|
+
resumedFromDisk = true;
|
|
1107
1200
|
session = this.sessionManager.resume(options.sessionId);
|
|
1108
1201
|
const cachedCompacted = this.compactedMessagesBySession.get(options.sessionId);
|
|
1109
1202
|
messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
|
|
@@ -1126,8 +1219,31 @@ export class Engine {
|
|
|
1126
1219
|
}
|
|
1127
1220
|
// Append new user message
|
|
1128
1221
|
const userMsg = { role: "user", content: userMessageContent };
|
|
1222
|
+
if (!claimClientMessageId(session, options?.clientMessageId, "submit")) {
|
|
1223
|
+
const usage = session.state.tokenUsage ?? {
|
|
1224
|
+
promptTokens: 0,
|
|
1225
|
+
completionTokens: 0,
|
|
1226
|
+
totalTokens: 0,
|
|
1227
|
+
};
|
|
1228
|
+
return {
|
|
1229
|
+
text: "",
|
|
1230
|
+
reason: "completed",
|
|
1231
|
+
sessionId: session.state.sessionId,
|
|
1232
|
+
turnCount: session.state.turnCount ?? 0,
|
|
1233
|
+
usage: {
|
|
1234
|
+
promptTokens: usage.promptTokens ?? 0,
|
|
1235
|
+
completionTokens: usage.completionTokens ?? 0,
|
|
1236
|
+
totalTokens: usage.totalTokens ?? 0,
|
|
1237
|
+
},
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
if (parsedTask.hasImages)
|
|
1241
|
+
freshImageMessage = userMsg;
|
|
1129
1242
|
messages.push(userMsg);
|
|
1130
|
-
session.transcript.appendMessage("user", userMessageContent, {
|
|
1243
|
+
session.transcript.appendMessage("user", userMessageContent, {
|
|
1244
|
+
injected: options?.injected === true,
|
|
1245
|
+
clientMessageId: options?.clientMessageId,
|
|
1246
|
+
});
|
|
1131
1247
|
// Flush "active" status to disk immediately. resume() set it in memory
|
|
1132
1248
|
// (session-manager.ts), but without this write the on-disk state.json
|
|
1133
1249
|
// still shows the previous run's terminal reason — so any external
|
|
@@ -1139,13 +1255,20 @@ export class Engine {
|
|
|
1139
1255
|
// Cold start: shape (2) reuses the host-supplied sid; shape (3)
|
|
1140
1256
|
// lets sessionManager generate one with nanoid.
|
|
1141
1257
|
session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider, options?.sessionId, this.config.isSubAgent === true ? getCurrentSid() : undefined, this.config.isSubAgent === true ? "subagent" : this.config.origin);
|
|
1142
|
-
|
|
1143
|
-
session
|
|
1258
|
+
const userMsg = { role: "user", content: userMessageContent };
|
|
1259
|
+
claimClientMessageId(session, options?.clientMessageId, "submit");
|
|
1260
|
+
if (parsedTask.hasImages)
|
|
1261
|
+
freshImageMessage = userMsg;
|
|
1262
|
+
messages = [userMsg];
|
|
1263
|
+
session.transcript.appendMessage("user", userMessageContent, {
|
|
1264
|
+
clientMessageId: options?.clientMessageId,
|
|
1265
|
+
});
|
|
1144
1266
|
// Save first user message as session summary — text only. The summary
|
|
1145
1267
|
// shows up in the session list; "[image]" is more informative than a
|
|
1146
1268
|
// truncated `[object Object]` when the prompt was purely visual.
|
|
1147
1269
|
const summarySrc = parsedTask.hasImages
|
|
1148
|
-
? parsedTask.text ||
|
|
1270
|
+
? parsedTask.text ||
|
|
1271
|
+
`[image${parsedTask.images.length > 1 ? `s × ${parsedTask.images.length}` : ""}]`
|
|
1149
1272
|
: taskText;
|
|
1150
1273
|
session.state.summary = summarySrc.slice(0, 80).replace(/\n/g, " ");
|
|
1151
1274
|
this.sessionManager.saveState(session.state);
|
|
@@ -1156,21 +1279,6 @@ export class Engine {
|
|
|
1156
1279
|
// with this value so `/undo` reverts exactly this turn's file changes.
|
|
1157
1280
|
// (Both resume and cold-start paths converge here.)
|
|
1158
1281
|
session.state.turnSeq = (session.state.turnSeq ?? 0) + 1;
|
|
1159
|
-
// Stamp the resolved session id for downstream logging.
|
|
1160
|
-
//
|
|
1161
|
-
// `setCurrentSid` updates the module-level fallback so any code path
|
|
1162
|
-
// running outside an ALS scope (bootstrap, /sid before a run starts)
|
|
1163
|
-
// still sees the latest sid.
|
|
1164
|
-
//
|
|
1165
|
-
// The real isolation comes from wrapping the rest of `run` in
|
|
1166
|
-
// `runWithSid(sid, async () => { ... })`: every `getCurrentSid()`
|
|
1167
|
-
// call inside that closure — including those inside `await`ed child
|
|
1168
|
-
// Engine.run() calls — reads sid from this scope's
|
|
1169
|
-
// AsyncLocalStorage binding, not the module global. Sibling Engines
|
|
1170
|
-
// running concurrently each get their own scope; an `enterSid` inside
|
|
1171
|
-
// a child mutates that child's scope only and doesn't leak back to
|
|
1172
|
-
// the parent's chain after `await child.run(...)` returns.
|
|
1173
|
-
setCurrentSid(session.state.sessionId);
|
|
1174
1282
|
// B2 / Gate 1: stamp the resolved sid onto the tool context so
|
|
1175
1283
|
// session-scoped side effects (background-agent completion
|
|
1176
1284
|
// notifications) attribute to the right session. toolCtx is created
|
|
@@ -1178,6 +1286,9 @@ export class Engine {
|
|
|
1178
1286
|
// the first point we can set it. After this assignment treat the
|
|
1179
1287
|
// field as immutable for the rest of the run.
|
|
1180
1288
|
toolCtx.sessionId = session.state.sessionId;
|
|
1289
|
+
toolCtx.setSessionWorkspace = (workspace) => {
|
|
1290
|
+
session.state.workspace = workspace;
|
|
1291
|
+
};
|
|
1181
1292
|
return runWithSid(session.state.sessionId, async () => {
|
|
1182
1293
|
recordSessionStart(session.state.sessionId, {
|
|
1183
1294
|
// Strip <codeshell-image> base64 payloads before they reach
|
|
@@ -1188,7 +1299,7 @@ export class Engine {
|
|
|
1188
1299
|
model: this.config.llm.model,
|
|
1189
1300
|
provider: this.config.llm.provider,
|
|
1190
1301
|
permissionMode: this.config.permissionMode ?? "acceptEdits",
|
|
1191
|
-
resumed:
|
|
1302
|
+
resumed: resumedFromDisk,
|
|
1192
1303
|
});
|
|
1193
1304
|
// Session-level hook: fired once per Engine.run() entry, regardless of
|
|
1194
1305
|
// cold-start vs resume. Handlers can return `messages` to inject a
|
|
@@ -1198,7 +1309,8 @@ export class Engine {
|
|
|
1198
1309
|
const sessionStartHook = await this.emitHook("on_session_start", {
|
|
1199
1310
|
sessionId: session.state.sessionId,
|
|
1200
1311
|
cwd,
|
|
1201
|
-
resumed:
|
|
1312
|
+
resumed: resumedFromDisk,
|
|
1313
|
+
source: resumedFromDisk ? "resume" : "startup",
|
|
1202
1314
|
});
|
|
1203
1315
|
// Per-turn hook: fired every time a new user prompt enters the loop.
|
|
1204
1316
|
// Equivalent to CC's UserPromptSubmit. Handlers can inject lightweight
|
|
@@ -1212,7 +1324,7 @@ export class Engine {
|
|
|
1212
1324
|
// and silently leaking attachment bytes through hooks is the kind of
|
|
1213
1325
|
// exfiltration risk a curious user-installed shell hook shouldn't carry.
|
|
1214
1326
|
prompt: taskText,
|
|
1215
|
-
resumed:
|
|
1327
|
+
resumed: resumedFromDisk,
|
|
1216
1328
|
});
|
|
1217
1329
|
// updatedPrompt: handler rewrote the user's prompt text. Replace the
|
|
1218
1330
|
// last user message we just pushed (cold-start: line ~511; resume:
|
|
@@ -1230,22 +1342,49 @@ export class Engine {
|
|
|
1230
1342
|
messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
|
|
1231
1343
|
}
|
|
1232
1344
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1345
|
+
const contextManager = new ContextManager({
|
|
1346
|
+
maxTokens: this.resolveMaxContextTokens(),
|
|
1347
|
+
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1348
|
+
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1349
|
+
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1350
|
+
});
|
|
1351
|
+
this.lastContextManager = contextManager;
|
|
1352
|
+
const persistedContextAnchor = session.state.contextUsageAnchor;
|
|
1353
|
+
const contextAnchorCompatible = persistedContextAnchor !== undefined &&
|
|
1354
|
+
(persistedContextAnchor.provider === undefined ||
|
|
1355
|
+
persistedContextAnchor.provider === this.config.llm.provider) &&
|
|
1356
|
+
(persistedContextAnchor.model === undefined ||
|
|
1357
|
+
persistedContextAnchor.model === this.config.llm.model) &&
|
|
1358
|
+
(persistedContextAnchor.messageCount <= messages.length ||
|
|
1359
|
+
persistedContextAnchor.estimateAtAnchor !== undefined);
|
|
1360
|
+
if (contextAnchorCompatible) {
|
|
1361
|
+
contextManager.seedActualUsage(persistedContextAnchor);
|
|
1362
|
+
}
|
|
1363
|
+
// Best-effort token estimate of the full prompt so the UI's ctx bar isn't
|
|
1364
|
+
// 0% before the first real usage_update arrives. The authoritative count
|
|
1235
1365
|
// comes from `usage.promptTokens` after the first LLM response — this is
|
|
1236
|
-
// just a display-friendly approximation for the first frame
|
|
1366
|
+
// just a display-friendly approximation for the first frame, annotated
|
|
1367
|
+
// with source/confidence so consumers don't treat heuristics as truth.
|
|
1237
1368
|
//
|
|
1238
1369
|
// Only seed once per (process, sid). On subsequent turns the UI already
|
|
1239
|
-
// shows the previous turn's accurate ctx; overwriting it with
|
|
1240
|
-
//
|
|
1370
|
+
// shows the previous turn's accurate ctx; overwriting it with a fresh
|
|
1371
|
+
// best-effort estimate would make the bar visibly drop on every submit.
|
|
1241
1372
|
const sid = session.state.sessionId;
|
|
1242
1373
|
const needsCtxSeed = !this.ctxSeedSent.has(sid);
|
|
1243
|
-
const
|
|
1244
|
-
?
|
|
1245
|
-
const
|
|
1246
|
-
return
|
|
1247
|
-
|
|
1248
|
-
|
|
1374
|
+
const ctxSeed = needsCtxSeed
|
|
1375
|
+
? (() => {
|
|
1376
|
+
const checked = contextManager.checkLimits(messages);
|
|
1377
|
+
return {
|
|
1378
|
+
tokens: checked.tokens,
|
|
1379
|
+
source: checked.promptTokensSource,
|
|
1380
|
+
confidence: checked.promptTokensConfidence,
|
|
1381
|
+
};
|
|
1382
|
+
})()
|
|
1383
|
+
: {
|
|
1384
|
+
tokens: 0,
|
|
1385
|
+
source: "heuristic_estimate",
|
|
1386
|
+
confidence: "low",
|
|
1387
|
+
};
|
|
1249
1388
|
if (needsCtxSeed)
|
|
1250
1389
|
this.ctxSeedSent.add(sid);
|
|
1251
1390
|
// Tell the client the sid *now* instead of waiting for run() to resolve.
|
|
@@ -1254,7 +1393,9 @@ export class Engine {
|
|
|
1254
1393
|
options?.onStream?.({
|
|
1255
1394
|
type: "session_started",
|
|
1256
1395
|
sessionId: sid,
|
|
1257
|
-
promptTokens:
|
|
1396
|
+
promptTokens: ctxSeed.tokens,
|
|
1397
|
+
promptTokensSource: ctxSeed.source,
|
|
1398
|
+
promptTokensConfidence: ctxSeed.confidence,
|
|
1258
1399
|
});
|
|
1259
1400
|
// Replay the last TodoWrite snapshot on resume so the UI's pinned
|
|
1260
1401
|
// task panel re-hydrates without the LLM needing to call TodoWrite
|
|
@@ -1281,12 +1422,14 @@ export class Engine {
|
|
|
1281
1422
|
// in this same session don't re-prompt. Headless/auto backends skip
|
|
1282
1423
|
// this — they don't prompt, so there are no project rules to persist.
|
|
1283
1424
|
if (approvalBackend instanceof InteractiveApprovalBackend) {
|
|
1284
|
-
approvalBackend.
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1425
|
+
approvalBackend.setSessionContext(session.state.sessionId, {
|
|
1426
|
+
cwd,
|
|
1427
|
+
onProjectRules: (rules) => {
|
|
1428
|
+
// Prepend the *full* accumulated list of session-saved project rules
|
|
1429
|
+
// so user approvals win over defaults and earlier approvals aren't
|
|
1430
|
+
// dropped when later ones come in.
|
|
1431
|
+
permission.reconfigure(mode, approvalBackend, [...rules, ...defaultRules]);
|
|
1432
|
+
},
|
|
1290
1433
|
});
|
|
1291
1434
|
}
|
|
1292
1435
|
const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
|
|
@@ -1302,13 +1445,6 @@ export class Engine {
|
|
|
1302
1445
|
// Wire abort signal for cascading cancellation + per-Engine ToolContext
|
|
1303
1446
|
toolExecutor.setSignal(options?.signal);
|
|
1304
1447
|
toolExecutor.setContext(toolCtx);
|
|
1305
|
-
const contextManager = new ContextManager({
|
|
1306
|
-
maxTokens: this.resolveMaxContextTokens(),
|
|
1307
|
-
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1308
|
-
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1309
|
-
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1310
|
-
});
|
|
1311
|
-
this.lastContextManager = contextManager;
|
|
1312
1448
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
1313
1449
|
const promptComposer = new PromptComposer({
|
|
1314
1450
|
cwd,
|
|
@@ -1370,6 +1506,7 @@ export class Engine {
|
|
|
1370
1506
|
(normalizeGoal(options?.goal) !== undefined ||
|
|
1371
1507
|
session.state.activeGoal !== undefined ||
|
|
1372
1508
|
normalizeGoal(this.config.goal) !== undefined),
|
|
1509
|
+
settingsScope: this.config.settingsScope ?? "project",
|
|
1373
1510
|
};
|
|
1374
1511
|
toolCtx.toolVisibility = toolVisibility;
|
|
1375
1512
|
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
@@ -1483,26 +1620,27 @@ export class Engine {
|
|
|
1483
1620
|
// run would be evaluated fresh and might get a different replacement
|
|
1484
1621
|
// string than the one already in the message, breaking idempotency.
|
|
1485
1622
|
contextManager.initReplacementStateFromMessages(messages);
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1623
|
+
// Two summarizers with DIFFERENT quality needs:
|
|
1624
|
+
//
|
|
1625
|
+
// 1. Context-compaction summary (setSummarizeFn) → PRIMARY model. This
|
|
1626
|
+
// condenses many rounds into the running summary that REPLACES the real
|
|
1627
|
+
// history; a dropped decision makes the conversation "forget" and poisons
|
|
1628
|
+
// every subsequent turn. It fires only near the compact ratio (~0.85), so
|
|
1629
|
+
// it's infrequent — quality far outweighs the occasional extra cost of a
|
|
1630
|
+
// primary-model call. (Manual /compact uses the primary for the same
|
|
1631
|
+
// reason; see forceCompact.)
|
|
1632
|
+
//
|
|
1633
|
+
// 2. Tool-use one-liner summaries (modelFacade.summarize below) → AUX model.
|
|
1634
|
+
// These are tiny throwaway outputs ("Wrote design doc") fired every turn;
|
|
1635
|
+
// that high-frequency, low-stakes chore is exactly what aux is for.
|
|
1492
1636
|
const auxSummaryClient = await this.resolveAuxClient(llmClient);
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
1501
|
-
// OpenAI-compatible provider the field is ignored.
|
|
1502
|
-
reasoning: { mode: "off" },
|
|
1503
|
-
});
|
|
1504
|
-
return summaryResponse.text;
|
|
1505
|
-
});
|
|
1637
|
+
Object.assign(session.state, normalizeCumulativeUsageCounters(session.state, session.state.tokenUsage));
|
|
1638
|
+
const recordCumulativeUsage = (usage) => {
|
|
1639
|
+
const next = addCumulativeUsage(session.state, usage);
|
|
1640
|
+
Object.assign(session.state, next);
|
|
1641
|
+
return next;
|
|
1642
|
+
};
|
|
1643
|
+
contextManager.setSummarizeFn(this.buildSummarizeFn(llmClient, recordCumulativeUsage));
|
|
1506
1644
|
// Create components (requires resolved llmClient).
|
|
1507
1645
|
const modelFacade = new ModelFacade(llmClient, session.transcript);
|
|
1508
1646
|
// Session-cumulative usage baseline: the LLM client is recreated per run
|
|
@@ -1676,7 +1814,16 @@ export class Engine {
|
|
|
1676
1814
|
pendingCompactInfo = null;
|
|
1677
1815
|
return info;
|
|
1678
1816
|
},
|
|
1679
|
-
consumeSteer: () => this.consumeSteer(sid),
|
|
1817
|
+
consumeSteer: (source) => this.consumeSteer(sid, source),
|
|
1818
|
+
claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
|
|
1819
|
+
recordCumulativeUsage,
|
|
1820
|
+
recordContextUsageAnchor: (anchor) => {
|
|
1821
|
+
session.state.contextUsageAnchor = {
|
|
1822
|
+
...anchor,
|
|
1823
|
+
provider: this.config.llm.provider,
|
|
1824
|
+
model: this.config.llm.model,
|
|
1825
|
+
};
|
|
1826
|
+
},
|
|
1680
1827
|
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1681
1828
|
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1682
1829
|
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
@@ -1715,6 +1862,7 @@ export class Engine {
|
|
|
1715
1862
|
maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 25,
|
|
1716
1863
|
onStream: options?.onStream,
|
|
1717
1864
|
signal: options?.signal,
|
|
1865
|
+
freshImageMessages: freshImageMessage ? [freshImageMessage] : undefined,
|
|
1718
1866
|
// Goal mode: the active goal is surfaced to the on_stop handler via
|
|
1719
1867
|
// ctx.data.goal; the GoalStopHook (registered above) judges it.
|
|
1720
1868
|
goal: normalizedGoal,
|
|
@@ -1727,16 +1875,25 @@ export class Engine {
|
|
|
1727
1875
|
// baseline + this run's running total (idempotent per boundary,
|
|
1728
1876
|
// accumulates across runs; carries cacheRead/cacheCreation too).
|
|
1729
1877
|
session.state.tokenUsage = foldRunUsage(usageBaseline, modelFacade.getUsage());
|
|
1730
|
-
// Surface the session
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
const
|
|
1878
|
+
// Surface the whole-session monotonic cache counts to the UI.
|
|
1879
|
+
// Separate from turn-loop's authoritative per-response emit (which
|
|
1880
|
+
// drives the live context reading and single-turn metric).
|
|
1881
|
+
const cumulative = normalizeCumulativeUsageCounters(session.state, session.state.tokenUsage);
|
|
1882
|
+
const cumulativeHitRate = cumulativeCacheHitRate(cumulative);
|
|
1734
1883
|
options?.onStream?.({
|
|
1735
1884
|
type: "usage_update",
|
|
1736
|
-
promptTokens:
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1885
|
+
promptTokens: cumulative.cumulativePromptTokens,
|
|
1886
|
+
promptTokensSource: "session_cumulative",
|
|
1887
|
+
promptTokensConfidence: "high",
|
|
1888
|
+
cumulativePromptTokens: cumulative.cumulativePromptTokens,
|
|
1889
|
+
cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
1890
|
+
cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
1891
|
+
...(cumulativeHitRate !== undefined
|
|
1892
|
+
? { cumulativeCacheHitRate: cumulativeHitRate }
|
|
1893
|
+
: {}),
|
|
1894
|
+
sessionPromptTokens: cumulative.cumulativePromptTokens,
|
|
1895
|
+
sessionCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
1896
|
+
sessionCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
1740
1897
|
});
|
|
1741
1898
|
if (this.config.costStore) {
|
|
1742
1899
|
session.state.costState = this.config.costStore.serialize();
|
|
@@ -1940,6 +2097,28 @@ export class Engine {
|
|
|
1940
2097
|
* active run's client) when unset, unknown, or on any build failure — aux
|
|
1941
2098
|
* work is best-effort and must never break a run.
|
|
1942
2099
|
*/
|
|
2100
|
+
/**
|
|
2101
|
+
* Build the SummarizeFn used for context compaction. Extracted so both the
|
|
2102
|
+
* run path and forceCompact share one definition of the summarization call.
|
|
2103
|
+
*/
|
|
2104
|
+
buildSummarizeFn(auxSummaryClient, recordCumulativeUsage) {
|
|
2105
|
+
return async (prompt) => {
|
|
2106
|
+
const summaryResponse = await auxSummaryClient.createMessage({
|
|
2107
|
+
systemPrompt: "You are a conversation summarizer. Be concise and factual.",
|
|
2108
|
+
messages: [{ role: "user", content: prompt }],
|
|
2109
|
+
tools: [],
|
|
2110
|
+
maxTokens: 1024,
|
|
2111
|
+
// Auxiliary call — no need to burn reasoning tokens. On DeepSeek V4
|
|
2112
|
+
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
2113
|
+
// OpenAI-compatible provider the field is ignored.
|
|
2114
|
+
reasoning: { mode: "off" },
|
|
2115
|
+
});
|
|
2116
|
+
if (summaryResponse.usage) {
|
|
2117
|
+
recordCumulativeUsage?.(summaryResponse.usage);
|
|
2118
|
+
}
|
|
2119
|
+
return summaryResponse.text;
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
1943
2122
|
async resolveAuxClient(fallback) {
|
|
1944
2123
|
let auxKey;
|
|
1945
2124
|
try {
|
|
@@ -2005,7 +2184,9 @@ export class Engine {
|
|
|
2005
2184
|
// extraction, which then padded the memory store with low-signal
|
|
2006
2185
|
// entries. 8 messages is roughly "more than a single back-and-forth"
|
|
2007
2186
|
// — substantive enough to be worth a durable note.
|
|
2008
|
-
const messages = transcript
|
|
2187
|
+
const messages = transcript
|
|
2188
|
+
.toMessages()
|
|
2189
|
+
.filter((m) => m.role === "user" || m.role === "assistant");
|
|
2009
2190
|
if (messages.length < 8)
|
|
2010
2191
|
return;
|
|
2011
2192
|
// Memory orchestrator + dream-loop calls are auxiliary LLM calls
|
|
@@ -2107,10 +2288,8 @@ export class Engine {
|
|
|
2107
2288
|
return entry;
|
|
2108
2289
|
}
|
|
2109
2290
|
/**
|
|
2110
|
-
* Zero
|
|
2111
|
-
*
|
|
2112
|
-
* cache-hit stats from the prior model are no longer meaningful. The next
|
|
2113
|
-
* run's baseline (snapshotted from state.tokenUsage) then starts from zero.
|
|
2291
|
+
* Zero the legacy/model-scoped token/cache usage window on disk. The
|
|
2292
|
+
* whole-session cumulative counters are intentionally left alone.
|
|
2114
2293
|
*/
|
|
2115
2294
|
resetSessionUsage(sessionId) {
|
|
2116
2295
|
const zero = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -2182,7 +2361,9 @@ export class Engine {
|
|
|
2182
2361
|
try {
|
|
2183
2362
|
chmodSync(file, 0o600);
|
|
2184
2363
|
}
|
|
2185
|
-
catch {
|
|
2364
|
+
catch {
|
|
2365
|
+
/* best-effort */
|
|
2366
|
+
}
|
|
2186
2367
|
}
|
|
2187
2368
|
catch (err) {
|
|
2188
2369
|
logger.warn(`persistActiveModel failed: ${err.message}`);
|
|
@@ -2243,7 +2424,6 @@ export class Engine {
|
|
|
2243
2424
|
refreshRuntimeConfig(patch, version) {
|
|
2244
2425
|
if (version <= this.lastAppliedConfigVersion)
|
|
2245
2426
|
return;
|
|
2246
|
-
const prevServers = this.config.mcpServers ?? {};
|
|
2247
2427
|
const prevPresetName = this.preset.name;
|
|
2248
2428
|
this.config = { ...this.config, ...patch };
|
|
2249
2429
|
// #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
|
|
@@ -2254,8 +2434,20 @@ export class Engine {
|
|
|
2254
2434
|
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
2255
2435
|
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
2256
2436
|
// tool set, that part of the change only lands on session restart.
|
|
2257
|
-
const prevTools = resolveBuiltinToolNames({
|
|
2258
|
-
|
|
2437
|
+
const prevTools = resolveBuiltinToolNames({
|
|
2438
|
+
preset: prevPresetName,
|
|
2439
|
+
host: this.config.builtinToolHost,
|
|
2440
|
+
})
|
|
2441
|
+
.slice()
|
|
2442
|
+
.sort()
|
|
2443
|
+
.join(",");
|
|
2444
|
+
const nextTools = resolveBuiltinToolNames({
|
|
2445
|
+
preset: nextPreset.name,
|
|
2446
|
+
host: this.config.builtinToolHost,
|
|
2447
|
+
})
|
|
2448
|
+
.slice()
|
|
2449
|
+
.sort()
|
|
2450
|
+
.join(",");
|
|
2259
2451
|
if (prevTools !== nextTools) {
|
|
2260
2452
|
logger.warn("engine.preset_reload.tool_set_change_needs_restart", {
|
|
2261
2453
|
from: prevPresetName,
|
|
@@ -2339,6 +2531,41 @@ export class Engine {
|
|
|
2339
2531
|
}
|
|
2340
2532
|
return had;
|
|
2341
2533
|
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Reset a session's workspace pointer back to its main root. If the session is
|
|
2536
|
+
* actively running, mutate that live SessionBundle first so the run's next
|
|
2537
|
+
* saveState cannot resurrect a stale worktree pointer.
|
|
2538
|
+
*/
|
|
2539
|
+
releaseSessionWorkspace(sessionId) {
|
|
2540
|
+
if (!sessionId || !this.sessionManager.exists(sessionId))
|
|
2541
|
+
return null;
|
|
2542
|
+
const mainRoot = this.sessionManager.readCwd(sessionId) ??
|
|
2543
|
+
(this.activeRunSession?.state.sessionId === sessionId
|
|
2544
|
+
? this.activeRunSession.state.cwd
|
|
2545
|
+
: undefined);
|
|
2546
|
+
if (!mainRoot)
|
|
2547
|
+
return null;
|
|
2548
|
+
const workspace = { root: mainRoot, kind: "main" };
|
|
2549
|
+
if (this.activeRunSession?.state.sessionId === sessionId) {
|
|
2550
|
+
this.activeRunSession.state.workspace = workspace;
|
|
2551
|
+
}
|
|
2552
|
+
try {
|
|
2553
|
+
const bundle = this.activeRunSession?.state.sessionId === sessionId
|
|
2554
|
+
? this.activeRunSession
|
|
2555
|
+
: this.sessionManager.resume(sessionId);
|
|
2556
|
+
bundle.state.workspace = workspace;
|
|
2557
|
+
this.sessionManager.saveState(bundle.state);
|
|
2558
|
+
}
|
|
2559
|
+
catch {
|
|
2560
|
+
try {
|
|
2561
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace);
|
|
2562
|
+
}
|
|
2563
|
+
catch {
|
|
2564
|
+
return null;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return workspace;
|
|
2568
|
+
}
|
|
2342
2569
|
injectContext(sessionId, content) {
|
|
2343
2570
|
const session = this.sessionManager.resume(sessionId);
|
|
2344
2571
|
session.transcript.appendMessage("assistant", content);
|
|
@@ -2354,14 +2581,13 @@ export class Engine {
|
|
|
2354
2581
|
* Force context compaction on a session.
|
|
2355
2582
|
* Returns token stats before/after.
|
|
2356
2583
|
*/
|
|
2357
|
-
forceCompact(sessionId) {
|
|
2584
|
+
async forceCompact(sessionId) {
|
|
2358
2585
|
const effectiveSessionId = sessionId ?? this.lastSessionId;
|
|
2359
2586
|
if (!effectiveSessionId) {
|
|
2360
2587
|
return { before: 0, after: 0, strategy: "none (no active session)" };
|
|
2361
2588
|
}
|
|
2362
2589
|
const session = this.sessionManager.resume(effectiveSessionId);
|
|
2363
|
-
const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ??
|
|
2364
|
-
session.transcript.toMessages();
|
|
2590
|
+
const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
|
|
2365
2591
|
const before = estimateTokens(sourceMessages);
|
|
2366
2592
|
let contextManager = this.lastContextManager;
|
|
2367
2593
|
if (!contextManager || this.lastSessionId !== effectiveSessionId) {
|
|
@@ -2373,7 +2599,45 @@ export class Engine {
|
|
|
2373
2599
|
contextManager.initReplacementStateFromMessages(sourceMessages);
|
|
2374
2600
|
this.lastContextManager = contextManager;
|
|
2375
2601
|
}
|
|
2376
|
-
|
|
2602
|
+
// Manual /compact emits its UI boundary at the protocol layer from the
|
|
2603
|
+
// final before/after result. Capture the tier here, but avoid reusing a
|
|
2604
|
+
// stale run callback retained on lastContextManager, which could otherwise
|
|
2605
|
+
// double-emit.
|
|
2606
|
+
let compactStrategy;
|
|
2607
|
+
contextManager.setOnCompact((info) => {
|
|
2608
|
+
if (info.after < info.before)
|
|
2609
|
+
compactStrategy = info.strategy;
|
|
2610
|
+
});
|
|
2611
|
+
// Manual /compact = maximum compaction NOW. The automatic ladder waits for
|
|
2612
|
+
// compactAtRatio (0.85 * window), so on a 1M-window model an 800k text-only
|
|
2613
|
+
// conversation sits under the gate and manage() only runs a no-op micro.
|
|
2614
|
+
// Wire a summarizeFn (the run path does this per-run; a cold forceCompact on
|
|
2615
|
+
// a resumed-but-never-run session has none) and call forceSummarize, which
|
|
2616
|
+
// ignores the ratio gate and always summarizes (falling back to snip/window).
|
|
2617
|
+
//
|
|
2618
|
+
// Use the PRIMARY model, not the aux model. Automatic background compaction
|
|
2619
|
+
// routes to aux to keep the high-frequency path cheap, but summarization is
|
|
2620
|
+
// a high-fidelity task (drop a decision and the conversation "forgets"), and
|
|
2621
|
+
// a manual /compact is a low-frequency, user-initiated request for quality.
|
|
2622
|
+
// The aux model is sized for tiny outputs (titles, memory extraction), so
|
|
2623
|
+
// downgrading the one compaction the user explicitly asked for is backwards.
|
|
2624
|
+
try {
|
|
2625
|
+
const primaryClient = await createLLMClient(this.config.llm, this.config.clientDefaults);
|
|
2626
|
+
Object.assign(session.state, normalizeCumulativeUsageCounters(session.state, session.state.tokenUsage));
|
|
2627
|
+
const recordCompactUsage = (usage) => {
|
|
2628
|
+
const next = addCumulativeUsage(session.state, usage);
|
|
2629
|
+
Object.assign(session.state, next);
|
|
2630
|
+
this.sessionManager.saveState(session.state);
|
|
2631
|
+
return next;
|
|
2632
|
+
};
|
|
2633
|
+
contextManager.setSummarizeFn(this.buildSummarizeFn(primaryClient, recordCompactUsage));
|
|
2634
|
+
}
|
|
2635
|
+
catch (err) {
|
|
2636
|
+
logger.warn("engine.force_compact_client_failed", {
|
|
2637
|
+
error: err.message,
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
const compacted = await contextManager.forceSummarize(sourceMessages);
|
|
2377
2641
|
const after = estimateTokens(compacted);
|
|
2378
2642
|
this.compactedMessagesBySession.set(effectiveSessionId, compacted);
|
|
2379
2643
|
this.lastSessionId = effectiveSessionId;
|
|
@@ -2381,7 +2645,7 @@ export class Engine {
|
|
|
2381
2645
|
return {
|
|
2382
2646
|
before,
|
|
2383
2647
|
after,
|
|
2384
|
-
strategy:
|
|
2648
|
+
strategy: after >= before ? "no compaction needed" : (compactStrategy ?? "compacted"),
|
|
2385
2649
|
};
|
|
2386
2650
|
}
|
|
2387
2651
|
stripUserContextMessage(messages, userContextMsg) {
|
|
@@ -2420,10 +2684,10 @@ export class Engine {
|
|
|
2420
2684
|
buildPermissionConfig(mode, cwd) {
|
|
2421
2685
|
const rules = [...this.preset.defaultPermissionRules];
|
|
2422
2686
|
// Memory tools: dream scope is the LLM's own workspace, so save/delete
|
|
2423
|
-
// there go through without prompting. user-scope save/delete
|
|
2424
|
-
//
|
|
2425
|
-
//
|
|
2426
|
-
//
|
|
2687
|
+
// there go through without prompting. user-scope save/delete have no
|
|
2688
|
+
// explicit allow rule here, so default-mode classifier fallback asks the
|
|
2689
|
+
// user to confirm modifications. RegisteredTool.permissionDefault is only
|
|
2690
|
+
// UI/metadata and is not read by the classifier.
|
|
2427
2691
|
rules.push({
|
|
2428
2692
|
tool: "MemorySave",
|
|
2429
2693
|
argsPattern: { scope: "^dream$" },
|
|
@@ -2475,7 +2739,11 @@ export class Engine {
|
|
|
2475
2739
|
backend = interactive;
|
|
2476
2740
|
}
|
|
2477
2741
|
else {
|
|
2478
|
-
backend = new HeadlessApprovalBackend(mode === "bypassPermissions"
|
|
2742
|
+
backend = new HeadlessApprovalBackend(mode === "bypassPermissions"
|
|
2743
|
+
? "approve-all"
|
|
2744
|
+
: mode === "dontAsk"
|
|
2745
|
+
? "deny-all"
|
|
2746
|
+
: "deny-all");
|
|
2479
2747
|
}
|
|
2480
2748
|
}
|
|
2481
2749
|
return { rules, backend };
|
|
@@ -2517,7 +2785,8 @@ export class Engine {
|
|
|
2517
2785
|
* rule set buildPermissionConfig does, without constructing a backend.
|
|
2518
2786
|
*/
|
|
2519
2787
|
getPermissionRules() {
|
|
2520
|
-
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd())
|
|
2788
|
+
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd())
|
|
2789
|
+
.rules;
|
|
2521
2790
|
}
|
|
2522
2791
|
/**
|
|
2523
2792
|
* Toggle plan mode directly. Called by the Plan tool (Task 7) via ToolContext.engine.
|
|
@@ -2604,12 +2873,8 @@ export class Engine {
|
|
|
2604
2873
|
getAgentDefinitions(cwd) {
|
|
2605
2874
|
const disabledAgents = this.readDisabledAgents(cwd);
|
|
2606
2875
|
const disabledPlugins = this.readDisabledLists().disabledPlugins;
|
|
2607
|
-
const disabledKey = [...disabledAgents, "::", ...disabledPlugins]
|
|
2608
|
-
|
|
2609
|
-
.sort()
|
|
2610
|
-
.join(" ");
|
|
2611
|
-
if (this.agentDefsCache?.cwd !== cwd ||
|
|
2612
|
-
this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2876
|
+
const disabledKey = [...disabledAgents, "::", ...disabledPlugins].slice().sort().join(" ");
|
|
2877
|
+
if (this.agentDefsCache?.cwd !== cwd || this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2613
2878
|
this.agentDefsCache = {
|
|
2614
2879
|
cwd,
|
|
2615
2880
|
disabledKey,
|
|
@@ -2681,6 +2946,25 @@ export class Engine {
|
|
|
2681
2946
|
}
|
|
2682
2947
|
return cached;
|
|
2683
2948
|
}
|
|
2949
|
+
resolveSandboxConfigForCwd(cwd) {
|
|
2950
|
+
// Priority: config.sandbox → project settings.sandbox → global → per-run
|
|
2951
|
+
// default. Read UNMERGED per-scope (getForScope) so a project that wrote no
|
|
2952
|
+
// sandbox genuinely follows global, rather than inheriting global's mode and
|
|
2953
|
+
// looking like it set one. Fixes "项目级配了不生效" + the scope model.
|
|
2954
|
+
let projectSandbox;
|
|
2955
|
+
let globalSandbox;
|
|
2956
|
+
try {
|
|
2957
|
+
const sm = this.getSettingsManager();
|
|
2958
|
+
if (this.config.isSubAgent !== true) {
|
|
2959
|
+
projectSandbox = sm.getForScope("project", cwd).sandbox;
|
|
2960
|
+
}
|
|
2961
|
+
globalSandbox = sm.getForScope("user").sandbox;
|
|
2962
|
+
}
|
|
2963
|
+
catch {
|
|
2964
|
+
// settings unavailable → fall through to per-run default
|
|
2965
|
+
}
|
|
2966
|
+
return resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
|
|
2967
|
+
}
|
|
2684
2968
|
/**
|
|
2685
2969
|
* Build the shell env layered onto the Bash tool / background shells (see
|
|
2686
2970
|
* mergeShellEnv). Three user-configured sources, merged lowest → highest:
|
|
@@ -2738,7 +3022,7 @@ export class Engine {
|
|
|
2738
3022
|
// the host user's credentials (same isolation contract as top-level env).
|
|
2739
3023
|
// Placed below settings.env so an explicit `env` entry can still override.
|
|
2740
3024
|
const credScope = (this.config.settingsScope ?? "project") === "full" ? "full" : "project";
|
|
2741
|
-
layer(
|
|
3025
|
+
layer(getCredentialAccess().envExposures(cwd, credScope));
|
|
2742
3026
|
layer(settings.env); // top-level env (global ⊕ project) wins
|
|
2743
3027
|
}
|
|
2744
3028
|
catch {
|
|
@@ -2775,9 +3059,34 @@ export class Engine {
|
|
|
2775
3059
|
return undefined;
|
|
2776
3060
|
}
|
|
2777
3061
|
}
|
|
3062
|
+
readWorktreeBranchPrefix(cwd) {
|
|
3063
|
+
if (this.config.isSubAgent === true || !cwd)
|
|
3064
|
+
return undefined;
|
|
3065
|
+
try {
|
|
3066
|
+
const settings = this.getSettingsManager().get();
|
|
3067
|
+
return settings.worktree?.branchPrefix;
|
|
3068
|
+
}
|
|
3069
|
+
catch {
|
|
3070
|
+
return undefined;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
async resolveWorktreeSetupSandbox(cwd) {
|
|
3074
|
+
if (!cwd)
|
|
3075
|
+
return undefined;
|
|
3076
|
+
const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
|
|
3077
|
+
const sandboxBackend = this.runtime
|
|
3078
|
+
? await this.runtime.resolveSandbox(sandboxConfig, cwd)
|
|
3079
|
+
: await this.resolveSandboxWithoutRuntime(sandboxConfig, cwd);
|
|
3080
|
+
return sandboxBackend.name === "off"
|
|
3081
|
+
? sandboxBackend
|
|
3082
|
+
: { ...sandboxBackend, network: sandboxConfig.network };
|
|
3083
|
+
}
|
|
3084
|
+
readWorktreeSetupShellEnv(cwd) {
|
|
3085
|
+
return this.readShellEnv(cwd);
|
|
3086
|
+
}
|
|
2778
3087
|
buildToolContext() {
|
|
2779
3088
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
2780
|
-
|
|
3089
|
+
const ctx = {
|
|
2781
3090
|
shellEnv: this.readShellEnv(this.config.cwd),
|
|
2782
3091
|
cwd: this.config.cwd ?? process.cwd(),
|
|
2783
3092
|
llmConfig: this.config.llm,
|
|
@@ -2785,6 +3094,7 @@ export class Engine {
|
|
|
2785
3094
|
toolRegistry: this.toolRegistry,
|
|
2786
3095
|
askUser: this.config.askUser,
|
|
2787
3096
|
browser: this.config.browserBridge,
|
|
3097
|
+
workspace: this.config.workspaceBridge,
|
|
2788
3098
|
injectCredentialToBrowser: this.config.injectCredentialToBrowser,
|
|
2789
3099
|
isSubAgent: this.config.isSubAgent === true,
|
|
2790
3100
|
// Credential tools narrow their disk reads to this scope: a project/
|
|
@@ -2805,6 +3115,10 @@ export class Engine {
|
|
|
2805
3115
|
// opts out via config. Otherwise allowed.
|
|
2806
3116
|
allowBackgroundShells: this.config.isSubAgent === true ? false : this.config.allowBackgroundShells !== false,
|
|
2807
3117
|
};
|
|
3118
|
+
ctx.setCwd = (cwd) => {
|
|
3119
|
+
ctx.cwd = cwd;
|
|
3120
|
+
};
|
|
3121
|
+
return ctx;
|
|
2808
3122
|
}
|
|
2809
3123
|
/**
|
|
2810
3124
|
* Read settings.disabledSkills + settings.disabledPlugins in a single
|