@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.16
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/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
- package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +7 -2
- package/dist/context/manager.d.ts +14 -2
- package/dist/context/manager.js +83 -8
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +29 -10
- package/dist/credentials/use-credential-tool.d.ts +1 -0
- package/dist/credentials/use-credential-tool.js +3 -0
- package/dist/engine/engine.d.ts +9 -0
- package/dist/engine/engine.js +115 -25
- package/dist/engine/turn-loop.d.ts +3 -1
- package/dist/engine/turn-loop.js +11 -2
- 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 -127
- package/dist/git/worktree.js +5 -464
- package/dist/index.d.ts +28 -27
- package/dist/index.js +24 -24
- 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/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- 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/preset/index.d.ts +1 -0
- package/dist/preset/index.js +6 -0
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.js +7 -0
- package/dist/protocol/server.d.ts +10 -0
- package/dist/protocol/server.js +90 -5
- package/dist/protocol/types.d.ts +6 -0
- package/dist/protocol/types.js +2 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- 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 +1 -1
- package/dist/session/session-manager.js +6 -6
- package/dist/settings/manager.js +43 -12
- 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 +2 -0
- package/dist/tool-system/builtin/background-jobs.js +4 -0
- 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/index.js +18 -8
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/worktree.d.ts +2 -0
- package/dist/tool-system/builtin/worktree.js +59 -11
- package/dist/tool-system/context.d.ts +11 -1
- package/dist/tool-system/mcp-manager.d.ts +8 -5
- package/dist/tool-system/mcp-manager.js +85 -55
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +28 -12
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
package/dist/engine/engine.js
CHANGED
|
@@ -42,12 +42,12 @@ import { MCPManager } from "../tool-system/mcp-manager.js";
|
|
|
42
42
|
import { SettingsManager, userHome } from "../settings/manager.js";
|
|
43
43
|
import { CredentialStore } from "../credentials/store.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";
|
|
@@ -445,6 +445,7 @@ export class Engine {
|
|
|
445
445
|
new ToolRegistry({
|
|
446
446
|
builtinTools: resolveBuiltinToolNames({
|
|
447
447
|
preset: this.preset.name,
|
|
448
|
+
host: config.builtinToolHost,
|
|
448
449
|
enabledBuiltinTools: builtinLists.enabledBuiltinTools,
|
|
449
450
|
disabledBuiltinTools: builtinLists.disabledBuiltinTools,
|
|
450
451
|
}),
|
|
@@ -635,6 +636,10 @@ export class Engine {
|
|
|
635
636
|
setBrowserBridge(bridge) {
|
|
636
637
|
this.config.browserBridge = bridge;
|
|
637
638
|
}
|
|
639
|
+
/** Inject the host-backed workspace bridge after construction. */
|
|
640
|
+
setWorkspaceBridge(bridge) {
|
|
641
|
+
this.config.workspaceBridge = bridge;
|
|
642
|
+
}
|
|
638
643
|
/**
|
|
639
644
|
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
640
645
|
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
@@ -736,7 +741,7 @@ export class Engine {
|
|
|
736
741
|
*/
|
|
737
742
|
async run(task, options) {
|
|
738
743
|
const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
|
|
739
|
-
? this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
744
|
+
? await this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
740
745
|
: undefined;
|
|
741
746
|
if (workspaceResume && !workspaceResume.ok) {
|
|
742
747
|
return {
|
|
@@ -971,6 +976,7 @@ export class Engine {
|
|
|
971
976
|
preset: this.preset.name,
|
|
972
977
|
enabledBuiltinTools: childEnabled,
|
|
973
978
|
disabledBuiltinTools: childDisabled,
|
|
979
|
+
builtinToolHost: this.config.builtinToolHost,
|
|
974
980
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
975
981
|
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
|
|
976
982
|
undefined,
|
|
@@ -1311,22 +1317,49 @@ export class Engine {
|
|
|
1311
1317
|
messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
|
|
1312
1318
|
}
|
|
1313
1319
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1320
|
+
const contextManager = new ContextManager({
|
|
1321
|
+
maxTokens: this.resolveMaxContextTokens(),
|
|
1322
|
+
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1323
|
+
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1324
|
+
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1325
|
+
});
|
|
1326
|
+
this.lastContextManager = contextManager;
|
|
1327
|
+
const persistedContextAnchor = session.state.contextUsageAnchor;
|
|
1328
|
+
const contextAnchorCompatible = persistedContextAnchor !== undefined &&
|
|
1329
|
+
(persistedContextAnchor.provider === undefined ||
|
|
1330
|
+
persistedContextAnchor.provider === this.config.llm.provider) &&
|
|
1331
|
+
(persistedContextAnchor.model === undefined ||
|
|
1332
|
+
persistedContextAnchor.model === this.config.llm.model) &&
|
|
1333
|
+
(persistedContextAnchor.messageCount <= messages.length ||
|
|
1334
|
+
persistedContextAnchor.estimateAtAnchor !== undefined);
|
|
1335
|
+
if (contextAnchorCompatible) {
|
|
1336
|
+
contextManager.seedActualUsage(persistedContextAnchor);
|
|
1337
|
+
}
|
|
1338
|
+
// Best-effort token estimate of the full prompt so the UI's ctx bar isn't
|
|
1339
|
+
// 0% before the first real usage_update arrives. The authoritative count
|
|
1316
1340
|
// comes from `usage.promptTokens` after the first LLM response — this is
|
|
1317
|
-
// just a display-friendly approximation for the first frame
|
|
1341
|
+
// just a display-friendly approximation for the first frame, annotated
|
|
1342
|
+
// with source/confidence so consumers don't treat heuristics as truth.
|
|
1318
1343
|
//
|
|
1319
1344
|
// Only seed once per (process, sid). On subsequent turns the UI already
|
|
1320
|
-
// shows the previous turn's accurate ctx; overwriting it with
|
|
1321
|
-
//
|
|
1345
|
+
// shows the previous turn's accurate ctx; overwriting it with a fresh
|
|
1346
|
+
// best-effort estimate would make the bar visibly drop on every submit.
|
|
1322
1347
|
const sid = session.state.sessionId;
|
|
1323
1348
|
const needsCtxSeed = !this.ctxSeedSent.has(sid);
|
|
1324
|
-
const
|
|
1325
|
-
?
|
|
1326
|
-
const
|
|
1327
|
-
return
|
|
1328
|
-
|
|
1329
|
-
|
|
1349
|
+
const ctxSeed = needsCtxSeed
|
|
1350
|
+
? (() => {
|
|
1351
|
+
const checked = contextManager.checkLimits(messages);
|
|
1352
|
+
return {
|
|
1353
|
+
tokens: checked.tokens,
|
|
1354
|
+
source: checked.promptTokensSource,
|
|
1355
|
+
confidence: checked.promptTokensConfidence,
|
|
1356
|
+
};
|
|
1357
|
+
})()
|
|
1358
|
+
: {
|
|
1359
|
+
tokens: 0,
|
|
1360
|
+
source: "heuristic_estimate",
|
|
1361
|
+
confidence: "low",
|
|
1362
|
+
};
|
|
1330
1363
|
if (needsCtxSeed)
|
|
1331
1364
|
this.ctxSeedSent.add(sid);
|
|
1332
1365
|
// Tell the client the sid *now* instead of waiting for run() to resolve.
|
|
@@ -1335,7 +1368,9 @@ export class Engine {
|
|
|
1335
1368
|
options?.onStream?.({
|
|
1336
1369
|
type: "session_started",
|
|
1337
1370
|
sessionId: sid,
|
|
1338
|
-
promptTokens:
|
|
1371
|
+
promptTokens: ctxSeed.tokens,
|
|
1372
|
+
promptTokensSource: ctxSeed.source,
|
|
1373
|
+
promptTokensConfidence: ctxSeed.confidence,
|
|
1339
1374
|
});
|
|
1340
1375
|
// Replay the last TodoWrite snapshot on resume so the UI's pinned
|
|
1341
1376
|
// task panel re-hydrates without the LLM needing to call TodoWrite
|
|
@@ -1383,13 +1418,6 @@ export class Engine {
|
|
|
1383
1418
|
// Wire abort signal for cascading cancellation + per-Engine ToolContext
|
|
1384
1419
|
toolExecutor.setSignal(options?.signal);
|
|
1385
1420
|
toolExecutor.setContext(toolCtx);
|
|
1386
|
-
const contextManager = new ContextManager({
|
|
1387
|
-
maxTokens: this.resolveMaxContextTokens(),
|
|
1388
|
-
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1389
|
-
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1390
|
-
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1391
|
-
});
|
|
1392
|
-
this.lastContextManager = contextManager;
|
|
1393
1421
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
1394
1422
|
const promptComposer = new PromptComposer({
|
|
1395
1423
|
cwd,
|
|
@@ -1451,6 +1479,7 @@ export class Engine {
|
|
|
1451
1479
|
(normalizeGoal(options?.goal) !== undefined ||
|
|
1452
1480
|
session.state.activeGoal !== undefined ||
|
|
1453
1481
|
normalizeGoal(this.config.goal) !== undefined),
|
|
1482
|
+
settingsScope: this.config.settingsScope ?? "project",
|
|
1454
1483
|
};
|
|
1455
1484
|
toolCtx.toolVisibility = toolVisibility;
|
|
1456
1485
|
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
@@ -1761,6 +1790,13 @@ export class Engine {
|
|
|
1761
1790
|
consumeSteer: (source) => this.consumeSteer(sid, source),
|
|
1762
1791
|
claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
|
|
1763
1792
|
recordCumulativeUsage,
|
|
1793
|
+
recordContextUsageAnchor: (anchor) => {
|
|
1794
|
+
session.state.contextUsageAnchor = {
|
|
1795
|
+
...anchor,
|
|
1796
|
+
provider: this.config.llm.provider,
|
|
1797
|
+
model: this.config.llm.model,
|
|
1798
|
+
};
|
|
1799
|
+
},
|
|
1764
1800
|
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1765
1801
|
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1766
1802
|
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
@@ -1820,6 +1856,8 @@ export class Engine {
|
|
|
1820
1856
|
options?.onStream?.({
|
|
1821
1857
|
type: "usage_update",
|
|
1822
1858
|
promptTokens: cumulative.cumulativePromptTokens,
|
|
1859
|
+
promptTokensSource: "session_cumulative",
|
|
1860
|
+
promptTokensConfidence: "high",
|
|
1823
1861
|
cumulativePromptTokens: cumulative.cumulativePromptTokens,
|
|
1824
1862
|
cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
|
|
1825
1863
|
cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
|
|
@@ -2359,7 +2397,6 @@ export class Engine {
|
|
|
2359
2397
|
refreshRuntimeConfig(patch, version) {
|
|
2360
2398
|
if (version <= this.lastAppliedConfigVersion)
|
|
2361
2399
|
return;
|
|
2362
|
-
const prevServers = this.config.mcpServers ?? {};
|
|
2363
2400
|
const prevPresetName = this.preset.name;
|
|
2364
2401
|
this.config = { ...this.config, ...patch };
|
|
2365
2402
|
// #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
|
|
@@ -2370,11 +2407,17 @@ export class Engine {
|
|
|
2370
2407
|
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
2371
2408
|
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
2372
2409
|
// tool set, that part of the change only lands on session restart.
|
|
2373
|
-
const prevTools = resolveBuiltinToolNames({
|
|
2410
|
+
const prevTools = resolveBuiltinToolNames({
|
|
2411
|
+
preset: prevPresetName,
|
|
2412
|
+
host: this.config.builtinToolHost,
|
|
2413
|
+
})
|
|
2374
2414
|
.slice()
|
|
2375
2415
|
.sort()
|
|
2376
2416
|
.join(",");
|
|
2377
|
-
const nextTools = resolveBuiltinToolNames({
|
|
2417
|
+
const nextTools = resolveBuiltinToolNames({
|
|
2418
|
+
preset: nextPreset.name,
|
|
2419
|
+
host: this.config.builtinToolHost,
|
|
2420
|
+
})
|
|
2378
2421
|
.slice()
|
|
2379
2422
|
.sort()
|
|
2380
2423
|
.join(",");
|
|
@@ -2461,6 +2504,41 @@ export class Engine {
|
|
|
2461
2504
|
}
|
|
2462
2505
|
return had;
|
|
2463
2506
|
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Reset a session's workspace pointer back to its main root. If the session is
|
|
2509
|
+
* actively running, mutate that live SessionBundle first so the run's next
|
|
2510
|
+
* saveState cannot resurrect a stale worktree pointer.
|
|
2511
|
+
*/
|
|
2512
|
+
releaseSessionWorkspace(sessionId) {
|
|
2513
|
+
if (!sessionId || !this.sessionManager.exists(sessionId))
|
|
2514
|
+
return null;
|
|
2515
|
+
const mainRoot = this.sessionManager.readCwd(sessionId) ??
|
|
2516
|
+
(this.activeRunSession?.state.sessionId === sessionId
|
|
2517
|
+
? this.activeRunSession.state.cwd
|
|
2518
|
+
: undefined);
|
|
2519
|
+
if (!mainRoot)
|
|
2520
|
+
return null;
|
|
2521
|
+
const workspace = { root: mainRoot, kind: "main" };
|
|
2522
|
+
if (this.activeRunSession?.state.sessionId === sessionId) {
|
|
2523
|
+
this.activeRunSession.state.workspace = workspace;
|
|
2524
|
+
}
|
|
2525
|
+
try {
|
|
2526
|
+
const bundle = this.activeRunSession?.state.sessionId === sessionId
|
|
2527
|
+
? this.activeRunSession
|
|
2528
|
+
: this.sessionManager.resume(sessionId);
|
|
2529
|
+
bundle.state.workspace = workspace;
|
|
2530
|
+
this.sessionManager.saveState(bundle.state);
|
|
2531
|
+
}
|
|
2532
|
+
catch {
|
|
2533
|
+
try {
|
|
2534
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace);
|
|
2535
|
+
}
|
|
2536
|
+
catch {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
return workspace;
|
|
2541
|
+
}
|
|
2464
2542
|
injectContext(sessionId, content) {
|
|
2465
2543
|
const session = this.sessionManager.resume(sessionId);
|
|
2466
2544
|
session.transcript.appendMessage("assistant", content);
|
|
@@ -2954,6 +3032,17 @@ export class Engine {
|
|
|
2954
3032
|
return undefined;
|
|
2955
3033
|
}
|
|
2956
3034
|
}
|
|
3035
|
+
readWorktreeBranchPrefix(cwd) {
|
|
3036
|
+
if (this.config.isSubAgent === true || !cwd)
|
|
3037
|
+
return undefined;
|
|
3038
|
+
try {
|
|
3039
|
+
const settings = this.getSettingsManager().get();
|
|
3040
|
+
return settings.worktree?.branchPrefix;
|
|
3041
|
+
}
|
|
3042
|
+
catch {
|
|
3043
|
+
return undefined;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
2957
3046
|
async resolveWorktreeSetupSandbox(cwd) {
|
|
2958
3047
|
if (!cwd)
|
|
2959
3048
|
return undefined;
|
|
@@ -2978,6 +3067,7 @@ export class Engine {
|
|
|
2978
3067
|
toolRegistry: this.toolRegistry,
|
|
2979
3068
|
askUser: this.config.askUser,
|
|
2980
3069
|
browser: this.config.browserBridge,
|
|
3070
|
+
workspace: this.config.workspaceBridge,
|
|
2981
3071
|
injectCredentialToBrowser: this.config.injectCredentialToBrowser,
|
|
2982
3072
|
isSubAgent: this.config.isSubAgent === true,
|
|
2983
3073
|
// Credential tools narrow their disk reads to this scope: a project/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Following Claude Code's po_() pattern:
|
|
5
5
|
* pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
|
|
6
6
|
*/
|
|
7
|
-
import type { Message, StreamCallback, TerminalReason, ContentBlock, ToolResult, TokenUsage } from "../types.js";
|
|
7
|
+
import type { Message, StreamCallback, TerminalReason, ContentBlock, ToolResult, TokenUsage, ContextUsageAnchor } from "../types.js";
|
|
8
8
|
import type { SteerItem } from "./steer-queue.js";
|
|
9
9
|
import { ModelFacade } from "./model-facade.js";
|
|
10
10
|
import { ToolExecutor } from "../tool-system/executor.js";
|
|
@@ -88,6 +88,8 @@ export interface TurnLoopDeps {
|
|
|
88
88
|
* Returns the updated counters so usage_update can carry both metric scopes.
|
|
89
89
|
*/
|
|
90
90
|
recordCumulativeUsage?: (usage: TokenUsage) => CumulativeUsageCounters;
|
|
91
|
+
/** Persist the latest context-estimation anchor derived from provider usage. */
|
|
92
|
+
recordContextUsageAnchor?: (anchor: ContextUsageAnchor) => void;
|
|
91
93
|
/**
|
|
92
94
|
* Reads/clears any user messages queued for THIS session via the steering
|
|
93
95
|
* channel (Engine.enqueueSteer) while a run is in flight. Consumed at the top
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -281,7 +281,12 @@ export class TurnLoop {
|
|
|
281
281
|
if (ctx === this.lastCtxEmit)
|
|
282
282
|
return;
|
|
283
283
|
this.lastCtxEmit = ctx;
|
|
284
|
-
this.config.onStream({
|
|
284
|
+
this.config.onStream({
|
|
285
|
+
type: "usage_update",
|
|
286
|
+
promptTokens: ctx,
|
|
287
|
+
promptTokensSource: overhead > 0 ? "calibrated_estimate" : "heuristic_estimate",
|
|
288
|
+
promptTokensConfidence: overhead > 0 ? "medium" : "low",
|
|
289
|
+
});
|
|
285
290
|
}
|
|
286
291
|
recordResponseUsage(usage) {
|
|
287
292
|
this.currentTurnUsage = addTokenUsage(this.currentTurnUsage, usage);
|
|
@@ -320,6 +325,8 @@ export class TurnLoop {
|
|
|
320
325
|
this.config.onStream({
|
|
321
326
|
type: "usage_update",
|
|
322
327
|
promptTokens,
|
|
328
|
+
promptTokensSource: "provider_usage",
|
|
329
|
+
promptTokensConfidence: "high",
|
|
323
330
|
...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
324
331
|
...(usage.cacheCreationTokens !== undefined
|
|
325
332
|
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
@@ -553,7 +560,9 @@ export class TurnLoop {
|
|
|
553
560
|
// compaction decisions use hybrid (actual + delta) estimation rather than
|
|
554
561
|
// pure heuristics. Without this the manager falls back to char/4 estimates.
|
|
555
562
|
if (response.usage?.promptTokens !== undefined) {
|
|
556
|
-
this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
|
|
563
|
+
const anchor = this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
|
|
564
|
+
if (anchor)
|
|
565
|
+
this.deps.recordContextUsageAnchor?.(anchor);
|
|
557
566
|
}
|
|
558
567
|
messages = this.markPendingImagesConsumed(messages);
|
|
559
568
|
// Truncation that cut off a TOOL CALL: the model overflowed
|
package/dist/engine/types.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface EngineConfig {
|
|
|
75
75
|
/** Browser automation bridge (browser_* tools). Wired by the host (desktop)
|
|
76
76
|
* after construction via setBrowserBridge. Undefined → tools degrade. */
|
|
77
77
|
browserBridge?: import("../tool-system/browser-bridge.js").BrowserBridge;
|
|
78
|
+
/** Host-backed workspace switch bridge (desktop only). */
|
|
79
|
+
workspaceBridge?: import("../tool-system/workspace-bridge.js").WorkspaceBridge;
|
|
78
80
|
/** Inject a cookie credential into the built-in browser (InjectCredential
|
|
79
81
|
* tool). Wired by the host after construction via setInjectCredential.
|
|
80
82
|
* Undefined → the tool degrades with a clear "no browser" error. */
|
|
@@ -127,6 +129,12 @@ export interface EngineConfig {
|
|
|
127
129
|
* automation shown; tui hidden). Sub-agents override to "subagent".
|
|
128
130
|
*/
|
|
129
131
|
origin?: SessionOrigin;
|
|
132
|
+
/**
|
|
133
|
+
* Host-specific builtin tool surface. Desktop replaces the legacy core
|
|
134
|
+
* worktree tools with SwitchSessionWorkspace because Electron main owns the
|
|
135
|
+
* authoritative workspace switch service and UI refresh path.
|
|
136
|
+
*/
|
|
137
|
+
builtinToolHost?: "desktop";
|
|
130
138
|
/**
|
|
131
139
|
* Optional shared EngineRuntime providing pre-constructed shared resources
|
|
132
140
|
* (modelPool, toolRegistry, etc.). When provided, Engine uses these instead
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SandboxBackend } from "../../tool-system/sandbox/index.js";
|
|
2
|
+
export interface WorktreeSession {
|
|
3
|
+
originalCwd: string;
|
|
4
|
+
worktreePath: string;
|
|
5
|
+
worktreeName: string;
|
|
6
|
+
worktreeBranch: string;
|
|
7
|
+
originalBranch?: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
/** Per-platform setup/cleanup scripts (a project's localEnvironment). */
|
|
12
|
+
export interface PlatformScripts {
|
|
13
|
+
default?: string;
|
|
14
|
+
macos?: string;
|
|
15
|
+
linux?: string;
|
|
16
|
+
windows?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CreateWorktreeOptions {
|
|
19
|
+
prefix?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pick the setup/cleanup script for the running platform, falling back to
|
|
23
|
+
* `default`. Empty/whitespace-only scripts are treated as absent so a project
|
|
24
|
+
* can leave a platform key blank without spawning an empty shell. `platform`
|
|
25
|
+
* defaults to `process.platform` so callers usually omit it; tests pass a
|
|
26
|
+
* fixed value.
|
|
27
|
+
*/
|
|
28
|
+
export declare function selectPlatformScript(scripts: PlatformScripts | undefined, platform?: NodeJS.Platform): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Create an isolated git worktree for an agent session.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createWorktree(cwd: string, slug: string, sessionId: string, opts?: CreateWorktreeOptions): Promise<WorktreeSession>;
|
|
33
|
+
export interface WorktreeSetupResult {
|
|
34
|
+
/** True if no setup script was configured for this platform (nothing ran). */
|
|
35
|
+
skipped: boolean;
|
|
36
|
+
/** True if a script ran and exited 0. */
|
|
37
|
+
ok: boolean;
|
|
38
|
+
/** Combined stdout/stderr, for surfacing to the user on failure. */
|
|
39
|
+
output: string;
|
|
40
|
+
/** Exit code when a script ran; undefined when skipped. */
|
|
41
|
+
exitCode?: number | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run a project's `localEnvironment.setupScripts` once, in the freshly-created
|
|
45
|
+
* worktree's root, right after `git worktree add`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runWorktreeSetup(worktreePath: string, script: string | undefined, opts?: {
|
|
48
|
+
sandbox?: SandboxBackend;
|
|
49
|
+
shellEnv?: Record<string, string>;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<WorktreeSetupResult>;
|
|
53
|
+
export interface RemoveWorktreeResult {
|
|
54
|
+
/** True once `git worktree remove` completed and the directory is gone. */
|
|
55
|
+
dirRemoved: boolean;
|
|
56
|
+
/** The branch targeted for deletion when removeBranch=true. */
|
|
57
|
+
branch?: string;
|
|
58
|
+
/** True when removeBranch=true and branch deletion completed. */
|
|
59
|
+
branchDeleted?: boolean;
|
|
60
|
+
/** Non-empty when the worktree directory is gone but branch deletion failed. */
|
|
61
|
+
branchError?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface RemoveWorktreeOptions {
|
|
64
|
+
prefix?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove a worktree and optionally its branch.
|
|
68
|
+
*/
|
|
69
|
+
export declare function removeWorktree(worktreePath: string, removeBranch?: boolean, opts?: RemoveWorktreeOptions): RemoveWorktreeResult;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { safeSpawnShell } from "../../runtime/safe-spawn.js";
|
|
4
|
+
import { buildSandboxEnv, defaultShellBinary, mergeShellEnv } from "../../runtime/spawn-common.js";
|
|
5
|
+
import { execGit, execGitSync, gitErrorMessage } from "./git-exec.js";
|
|
6
|
+
import { assertBranchNotCheckedOut, currentBranch, findGitRoot } from "./query.js";
|
|
7
|
+
import { applyPrefix, isManagedWorktreeBranch, validateWorktreeSlug } from "./slug.js";
|
|
8
|
+
/**
|
|
9
|
+
* Pick the setup/cleanup script for the running platform, falling back to
|
|
10
|
+
* `default`. Empty/whitespace-only scripts are treated as absent so a project
|
|
11
|
+
* can leave a platform key blank without spawning an empty shell. `platform`
|
|
12
|
+
* defaults to `process.platform` so callers usually omit it; tests pass a
|
|
13
|
+
* fixed value.
|
|
14
|
+
*/
|
|
15
|
+
export function selectPlatformScript(scripts, platform = process.platform) {
|
|
16
|
+
if (!scripts)
|
|
17
|
+
return undefined;
|
|
18
|
+
const key = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux";
|
|
19
|
+
// A blank platform key shouldn't shadow a real `default` — fall through.
|
|
20
|
+
const platformScript = scripts[key]?.trim() ? scripts[key] : undefined;
|
|
21
|
+
const candidate = platformScript ?? scripts.default;
|
|
22
|
+
const trimmed = candidate?.trim();
|
|
23
|
+
return trimmed ? candidate : undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create an isolated git worktree for an agent session.
|
|
27
|
+
*/
|
|
28
|
+
export async function createWorktree(cwd, slug, sessionId, opts = {}) {
|
|
29
|
+
validateWorktreeSlug(slug);
|
|
30
|
+
const gitRoot = await findGitRoot(cwd);
|
|
31
|
+
const branchName = applyPrefix(opts.prefix, slug, sessionId);
|
|
32
|
+
const worktreePath = resolve(gitRoot, "..", `.worktrees/${slug}-${sessionId.slice(0, 8)}`);
|
|
33
|
+
await assertBranchNotCheckedOut(gitRoot, branchName);
|
|
34
|
+
const originalBranch = await currentBranch(gitRoot);
|
|
35
|
+
// The argv form keeps branchName/worktreePath as literal positional
|
|
36
|
+
// arguments even if a future caller bypasses validateWorktreeSlug.
|
|
37
|
+
await execGit(gitRoot, ["worktree", "add", "-b", branchName, worktreePath], 30000);
|
|
38
|
+
// Symlink large directories to avoid disk bloat.
|
|
39
|
+
symlinkLargeDirectories(gitRoot, worktreePath);
|
|
40
|
+
return {
|
|
41
|
+
originalCwd: cwd,
|
|
42
|
+
worktreePath,
|
|
43
|
+
worktreeName: slug,
|
|
44
|
+
worktreeBranch: branchName,
|
|
45
|
+
originalBranch,
|
|
46
|
+
sessionId,
|
|
47
|
+
createdAt: Date.now(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run a project's `localEnvironment.setupScripts` once, in the freshly-created
|
|
52
|
+
* worktree's root, right after `git worktree add`.
|
|
53
|
+
*/
|
|
54
|
+
export async function runWorktreeSetup(worktreePath, script, opts = {}) {
|
|
55
|
+
const trimmed = script?.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return { skipped: true, ok: true, output: "" };
|
|
58
|
+
const shell = defaultShellBinary();
|
|
59
|
+
const backend = opts.sandbox;
|
|
60
|
+
const baseEnv = backend && backend.name !== "off" ? buildSandboxEnv() : { ...process.env };
|
|
61
|
+
const env = mergeShellEnv(baseEnv, opts.shellEnv);
|
|
62
|
+
const result = await safeSpawnShell(trimmed, {
|
|
63
|
+
cwd: worktreePath,
|
|
64
|
+
env,
|
|
65
|
+
timeoutMs: opts.timeoutMs ?? 120_000,
|
|
66
|
+
maxOutputBytes: 1024 * 1024,
|
|
67
|
+
sandbox: backend,
|
|
68
|
+
shell,
|
|
69
|
+
signal: opts.signal,
|
|
70
|
+
});
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (result.stdout)
|
|
73
|
+
parts.push(result.stdout);
|
|
74
|
+
if (result.stderr)
|
|
75
|
+
parts.push(result.stderr);
|
|
76
|
+
const output = parts.join("\n").trim();
|
|
77
|
+
if (result.aborted)
|
|
78
|
+
return { skipped: false, ok: false, output: output || "setup aborted" };
|
|
79
|
+
if (result.timedOut)
|
|
80
|
+
return { skipped: false, ok: false, output: output || "setup timed out" };
|
|
81
|
+
if (result.spawnFailed) {
|
|
82
|
+
return { skipped: false, ok: false, output: result.error ?? "failed to spawn setup script" };
|
|
83
|
+
}
|
|
84
|
+
return { skipped: false, ok: result.exitCode === 0, output, exitCode: result.exitCode };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Remove a worktree and optionally its branch.
|
|
88
|
+
*/
|
|
89
|
+
export function removeWorktree(worktreePath, removeBranch = false, opts = {}) {
|
|
90
|
+
// The MAIN repo root, not the worktree's own toplevel. `git rev-parse
|
|
91
|
+
// --show-toplevel` from inside a worktree returns the worktree path, which
|
|
92
|
+
// is about to be deleted; the branch-delete must run from the main repo,
|
|
93
|
+
// which outlives the worktree. Derive it from the common git dir.
|
|
94
|
+
let mainRoot;
|
|
95
|
+
try {
|
|
96
|
+
const commonDir = execGitSync(worktreePath, ["rev-parse", "--path-format=absolute", "--git-common-dir"], 5000).trim();
|
|
97
|
+
mainRoot = dirname(commonDir);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
throw new Error(`failed to inspect worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
|
|
101
|
+
cause: err,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Capture the worktree's branch BEFORE removing the worktree — afterwards
|
|
105
|
+
// the directory is gone and `git branch --show-current` in it would fail.
|
|
106
|
+
let branch = "";
|
|
107
|
+
if (removeBranch) {
|
|
108
|
+
try {
|
|
109
|
+
branch = execGitSync(worktreePath, ["branch", "--show-current"], 5000).trim();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new Error(`failed to determine branch for worktree ${worktreePath}: ${gitErrorMessage(err)}`, { cause: err });
|
|
113
|
+
}
|
|
114
|
+
if (!branch) {
|
|
115
|
+
throw new Error(`failed to determine branch for worktree ${worktreePath}`);
|
|
116
|
+
}
|
|
117
|
+
if (!isManagedWorktreeBranch(branch, opts.prefix)) {
|
|
118
|
+
throw new Error(`refusing to delete non-CodeShell worktree branch ${branch}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
execGitSync(mainRoot, ["worktree", "remove", worktreePath, "--force"], 30000);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
throw new Error(`failed to remove worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
|
|
126
|
+
cause: err,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (removeBranch) {
|
|
130
|
+
try {
|
|
131
|
+
execGitSync(mainRoot, ["branch", "-D", branch], 10000);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
dirRemoved: true,
|
|
136
|
+
branch,
|
|
137
|
+
branchDeleted: false,
|
|
138
|
+
branchError: gitErrorMessage(err),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return removeBranch ? { dirRemoved: true, branch, branchDeleted: true } : { dirRemoved: true };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Symlink large directories (node_modules, .venv, etc.) from main repo to
|
|
146
|
+
* worktree. Also links each monorepo workspace package's private
|
|
147
|
+
* `node_modules` (e.g. `packages/desktop/node_modules`, where bun keeps
|
|
148
|
+
* un-hoisted deps like electron) so a fresh worktree can build/run the whole
|
|
149
|
+
* workspace, not just the packages whose deps happen to be hoisted to the root.
|
|
150
|
+
*/
|
|
151
|
+
function symlinkLargeDirectories(sourceRoot, worktreePath) {
|
|
152
|
+
const largeDirs = ["node_modules", ".venv", "vendor", ".pnpm-store"];
|
|
153
|
+
for (const dir of largeDirs) {
|
|
154
|
+
linkDir(join(sourceRoot, dir), join(worktreePath, dir));
|
|
155
|
+
}
|
|
156
|
+
// Monorepo workspace packages keep private node_modules that are NOT hoisted
|
|
157
|
+
// to the root. Link `<pkg>/node_modules` for each package under `packages/`.
|
|
158
|
+
const packagesDir = join(sourceRoot, "packages");
|
|
159
|
+
if (!dirExists(packagesDir))
|
|
160
|
+
return;
|
|
161
|
+
let pkgNames;
|
|
162
|
+
try {
|
|
163
|
+
pkgNames = readdirSync(packagesDir);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const pkg of pkgNames) {
|
|
169
|
+
const source = join(packagesDir, pkg, "node_modules");
|
|
170
|
+
if (!dirExists(source))
|
|
171
|
+
continue;
|
|
172
|
+
const targetPkgDir = join(worktreePath, "packages", pkg);
|
|
173
|
+
// The worktree only has package dirs that exist in this branch's tree; a
|
|
174
|
+
// package present in the main repo but not checked out here is skipped.
|
|
175
|
+
if (!dirExists(targetPkgDir)) {
|
|
176
|
+
try {
|
|
177
|
+
mkdirSync(targetPkgDir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
linkDir(source, join(targetPkgDir, "node_modules"));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function dirExists(path) {
|
|
187
|
+
try {
|
|
188
|
+
return lstatSync(path).isDirectory();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function linkDir(source, target) {
|
|
195
|
+
// Windows directory symlinks need admin/Developer Mode and throw EPERM for a
|
|
196
|
+
// normal user; NTFS junctions don't and behave the same for our purpose.
|
|
197
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
198
|
+
if (dirExists(source) && !existsSync(target)) {
|
|
199
|
+
try {
|
|
200
|
+
symlinkSync(source, target, linkType);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Symlink/junction might fail on some systems — non-fatal.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface WorktreeDiffSummary {
|
|
2
|
+
/** Best-effort base used for the branch comparison, usually main/master. */
|
|
3
|
+
baseRef?: string;
|
|
4
|
+
/** Unique files changed either by commits ahead of base or by uncommitted changes. */
|
|
5
|
+
changedFiles: number;
|
|
6
|
+
/** Commits reachable from HEAD but not from baseRef. */
|
|
7
|
+
aheadCommits: number;
|
|
8
|
+
/** True when `git status --porcelain` reports local changes. */
|
|
9
|
+
hasUncommittedChanges: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function getWorktreeDiff(worktreePath: string, baseRef?: string): Promise<WorktreeDiffSummary>;
|
|
12
|
+
export declare function worktreeHasUncommittedChanges(worktreePath: string): Promise<boolean>;
|
|
13
|
+
export declare function worktreeHasUncommittedOrAheadChanges(worktreePath: string, baseRef?: string): Promise<boolean>;
|
|
14
|
+
export declare function findComparisonBaseRef(cwd: string): Promise<string | undefined>;
|