@cjhyy/code-shell-core 0.8.7 → 0.8.8
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/cli/agent-server-stdio.js +7 -5
- package/dist/context/manager.js +10 -2
- package/dist/engine/engine.d.ts +60 -8
- package/dist/engine/engine.js +153 -16
- package/dist/engine/run-setup.d.ts +1 -0
- package/dist/engine/run-setup.js +2 -1
- package/dist/engine/run-types.d.ts +7 -0
- package/dist/engine/turn-loop.d.ts +0 -3
- package/dist/engine/turn-loop.js +1 -80
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/panel-apps/manifest.d.ts +11 -11
- package/dist/panel-apps/manifest.js +4 -2
- package/dist/prompt/composer.d.ts +7 -0
- package/dist/prompt/composer.js +15 -8
- package/dist/protocol/server.js +40 -1
- package/dist/protocol/types.d.ts +14 -0
- package/dist/session/memory.d.ts +10 -0
- package/dist/session/memory.js +7 -1
- package/dist/session/session-manager.js +1 -0
- package/dist/session/transcript.d.ts +36 -0
- package/dist/session/transcript.js +149 -4
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -215,11 +215,13 @@ function freshSettings() {
|
|
|
215
215
|
}
|
|
216
216
|
const chatManager = new ChatSessionManager({
|
|
217
217
|
runtime,
|
|
218
|
-
// resolvedLlmConfig is the bootstrap-time snapshot.
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
218
|
+
// resolvedLlmConfig is the bootstrap-time snapshot. Explicit global model
|
|
219
|
+
// changes can move modelPool.activeKey ahead of it, so newly-created
|
|
220
|
+
// sessions re-resolve from the pool each time the factory fires. A
|
|
221
|
+
// per-session configure intentionally does NOT move that shared key; this
|
|
222
|
+
// keeps one Session's selected model from leaking into the next Session.
|
|
223
|
+
// Fall back to the snapshot only when the pool can't resolve (no active key
|
|
224
|
+
// — shouldn't happen in practice).
|
|
223
225
|
engineFactory: (slice) => {
|
|
224
226
|
// Effective cwd for THIS session: explicit slice.cwd → that project; absent
|
|
225
227
|
// → the no-repo sandbox (NOT the worker's stale boot cwd). See
|
package/dist/context/manager.js
CHANGED
|
@@ -547,8 +547,16 @@ export class ContextManager {
|
|
|
547
547
|
const window = messages.slice(start, end);
|
|
548
548
|
// Feed a prior anchored summary back so the LLM merge-updates rather than
|
|
549
549
|
// re-summarizes from scratch, matching the rolling-summary behavior of
|
|
550
|
-
// trySummaryCompact()
|
|
551
|
-
|
|
550
|
+
// trySummaryCompact() — but ONLY when that anchored summary sits inside
|
|
551
|
+
// the window being replaced. A summary inside [start, end) is about to be
|
|
552
|
+
// overwritten by this call, so it must be merged in or its content is
|
|
553
|
+
// lost. A summary OUTSIDE the window belongs to a different, untouched
|
|
554
|
+
// span and must NOT be merged in: doing so (via `?? this.lastSummary`,
|
|
555
|
+
// which is a single most-recent-summary carryover with no span
|
|
556
|
+
// awareness) made every subsequent range archive fold in the text of
|
|
557
|
+
// every prior one, so N archived spans produced N copies of the same
|
|
558
|
+
// growing summary instead of N independent summaries.
|
|
559
|
+
const priorSummary = extractAnchoredSummary(window);
|
|
552
560
|
const prompt = buildSummarizationPrompt(window, priorSummary);
|
|
553
561
|
const summary = await this.summarizeFn(prompt, opts.signal);
|
|
554
562
|
if (!summary || summary.length <= 50)
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -457,6 +457,13 @@ export declare class Engine {
|
|
|
457
457
|
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
458
458
|
* Returns the new model entry.
|
|
459
459
|
*
|
|
460
|
+
* Per-session switches (`persist:false`) must not mutate the shared
|
|
461
|
+
* ModelPool active key. Every desktop ChatSession owns an Engine, but those
|
|
462
|
+
* Engines share one runtime pool; changing the pool selection here made the
|
|
463
|
+
* next newly-created Session inherit whichever model another Session had
|
|
464
|
+
* just selected (for example a Mimi manager model leaking into its delegated
|
|
465
|
+
* Work Session).
|
|
466
|
+
*
|
|
460
467
|
* Persists settings.defaults.text (= the connection id / pool key) so the
|
|
461
468
|
* next process startup defaults to the same model — without this, switches
|
|
462
469
|
* only live in memory and every restart reverts to the previously persisted
|
|
@@ -623,22 +630,67 @@ export declare class Engine {
|
|
|
623
630
|
*/
|
|
624
631
|
private prepareContextManagerForSession;
|
|
625
632
|
/**
|
|
626
|
-
* Archive a
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
633
|
+
* Archive a contiguous message window of a session into a single anchored
|
|
634
|
+
* summary, leaving everything outside the window untouched, and cache the
|
|
635
|
+
* result so a later forceCompact/resume reads the archived history. This is
|
|
636
|
+
* a generic range-archival facade over ContextManager.summarizeRange.
|
|
637
|
+
* Returns token stats before/after; equal before/after means the window was
|
|
638
|
+
* empty/unresolvable or the summary was rejected.
|
|
639
|
+
*
|
|
640
|
+
* Window resolution depends on whether the caller supplied anchors:
|
|
641
|
+
* - With `anchors`, the caller's `range` is IGNORED and the window is
|
|
642
|
+
* resolved from the client-message-id anchors over a fresh, marker-aware
|
|
643
|
+
* transcript replay. Callers (the pet segment closure) compute index
|
|
644
|
+
* ranges over the RAW transcript message list, whose indices grow
|
|
645
|
+
* forever — but the live list is trimmed by every persisted marker
|
|
646
|
+
* (replay after restart) and by each in-process archival, so a raw index
|
|
647
|
+
* range clamps to an empty window or, worse, onto the WRONG tail
|
|
648
|
+
* messages, which would then be persistently mis-summarized against
|
|
649
|
+
* correct anchors. Anchors are the only stable coordinates.
|
|
650
|
+
* - Without `anchors`, legacy behavior: `range` is a half-open index window
|
|
651
|
+
* over the cached/in-process message list, and nothing is persisted.
|
|
634
652
|
*/
|
|
635
653
|
archiveTurnRange(sessionId: string, range: {
|
|
636
654
|
start: number;
|
|
637
655
|
end: number;
|
|
656
|
+
}, anchors?: {
|
|
657
|
+
toClientMessageId: string;
|
|
658
|
+
fromClientMessageId?: string;
|
|
659
|
+
segmentId?: string;
|
|
638
660
|
}): Promise<{
|
|
639
661
|
before: number;
|
|
640
662
|
after: number;
|
|
641
663
|
}>;
|
|
664
|
+
/**
|
|
665
|
+
* Guard against writing a marker anchored to a client message id the
|
|
666
|
+
* transcript doesn't actually contain. toMessages()'s replay silently
|
|
667
|
+
* ignores a range_archive event whose anchor doesn't resolve (fail open),
|
|
668
|
+
* so a bad anchor wouldn't corrupt anything on its own — but if the marker
|
|
669
|
+
* carries a segmentId, appendRangeArchive's idempotency check treats that
|
|
670
|
+
* segmentId as "already recorded" forever, with no retry path (e.g. a
|
|
671
|
+
* one-time migration keyed "migration-v1" would be permanently burned on
|
|
672
|
+
* its first, failed attempt). Reject before writing so the caller can
|
|
673
|
+
* retry with a corrected anchor. fromClientMessageId is optional in the
|
|
674
|
+
* data model (absent means "from the beginning"), so only validate it when
|
|
675
|
+
* present; missing it is fail-open by design, same as the replay path.
|
|
676
|
+
*/
|
|
677
|
+
private hasLiveArchiveAnchors;
|
|
678
|
+
/**
|
|
679
|
+
* Persist an archive boundary WITHOUT a summarization call — the caller
|
|
680
|
+
* already has the summary text (e.g. the one-time migration built from
|
|
681
|
+
* pet journal entries). Wraps the plain text in the anchored-summary
|
|
682
|
+
* envelope so replay and rolling-merge treat it like a real archive.
|
|
683
|
+
* Returns false when the segmentId was already recorded (idempotent) OR
|
|
684
|
+
* when the anchors don't resolve to real messages in this transcript —
|
|
685
|
+
* see hasLiveArchiveAnchors for why a dead anchor must be rejected before
|
|
686
|
+
* the (potentially one-shot) segmentId gets burned.
|
|
687
|
+
*/
|
|
688
|
+
appendArchiveMarker(sessionId: string, marker: {
|
|
689
|
+
summary: string;
|
|
690
|
+
toClientMessageId: string;
|
|
691
|
+
fromClientMessageId?: string;
|
|
692
|
+
segmentId?: string;
|
|
693
|
+
}): Promise<boolean>;
|
|
642
694
|
private recordCacheReadDiagnostics;
|
|
643
695
|
private getSettingsManager;
|
|
644
696
|
/**
|
package/dist/engine/engine.js
CHANGED
|
@@ -12,6 +12,7 @@ import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-q
|
|
|
12
12
|
import { RunEnvironmentResolver } from "./run-environment.js";
|
|
13
13
|
import { BUILTIN_TOOLS, } from "../tool-system/builtin/index.js";
|
|
14
14
|
import { asyncAgentRegistry } from "../tool-system/builtin/agent-registry.js";
|
|
15
|
+
import { skillToolDef } from "../tool-system/builtin/skill.js";
|
|
15
16
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
16
17
|
import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
|
|
17
18
|
import { HookRegistry } from "../hooks/registry.js";
|
|
@@ -20,7 +21,7 @@ import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
|
|
|
20
21
|
import { pluginAgentDirs } from "../plugins/installer/loadPluginAgents.js";
|
|
21
22
|
import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
|
|
22
23
|
import { ContextManager } from "../context/manager.js";
|
|
23
|
-
import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
|
|
24
|
+
import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildAnchoredSummaryMessage, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
|
|
24
25
|
import { PromptComposer } from "../prompt/composer.js";
|
|
25
26
|
import { SessionManager, assertSafeSessionId, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
|
|
26
27
|
import { createRunUsageAccounting, wireRunModelFacade } from "./run-accounting.js";
|
|
@@ -1935,6 +1936,10 @@ export class Engine {
|
|
|
1935
1936
|
(visibilityStoredGoal !== undefined && visibilityStoredGoal.paused !== true) ||
|
|
1936
1937
|
(visibilityDefaultGoal !== undefined && visibilityDefaultGoal.paused !== true));
|
|
1937
1938
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists(cwd, sessionProfileOverrides);
|
|
1939
|
+
// A profile whose tool allowlist excludes the Skill tool can never invoke
|
|
1940
|
+
// a skill, so the full skills listing would be dead context for every one
|
|
1941
|
+
// of its turns (e.g. the Pet manager) — inject none via an empty allowlist.
|
|
1942
|
+
const profileCanUseSkills = !profile?.allowedToolNames || profile.allowedToolNames.has(skillToolDef.name);
|
|
1938
1943
|
const promptComposer = new PromptComposer(buildPromptComposerConfig({
|
|
1939
1944
|
cwd,
|
|
1940
1945
|
model: this.config.llm.model,
|
|
@@ -1954,8 +1959,9 @@ export class Engine {
|
|
|
1954
1959
|
instructionBoundaryFinder: (scanCwd) => resolveInstructionBoundary(scanCwd, this.capabilities),
|
|
1955
1960
|
disabledSkills,
|
|
1956
1961
|
disabledPlugins,
|
|
1957
|
-
skillAllowlist: this.config.skillAllowlist,
|
|
1962
|
+
skillAllowlist: profileCanUseSkills ? this.config.skillAllowlist : [],
|
|
1958
1963
|
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1964
|
+
memoryCurrentProjectOnly: profile?.memoryCurrentProjectOnly,
|
|
1959
1965
|
goalToolState: !profile?.allowedToolNames ||
|
|
1960
1966
|
profile.allowedToolNames.has("complete_goal") ||
|
|
1961
1967
|
profile.allowedToolNames.has("cancel_goal")
|
|
@@ -2228,13 +2234,28 @@ export class Engine {
|
|
|
2228
2234
|
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
2229
2235
|
* Returns the new model entry.
|
|
2230
2236
|
*
|
|
2237
|
+
* Per-session switches (`persist:false`) must not mutate the shared
|
|
2238
|
+
* ModelPool active key. Every desktop ChatSession owns an Engine, but those
|
|
2239
|
+
* Engines share one runtime pool; changing the pool selection here made the
|
|
2240
|
+
* next newly-created Session inherit whichever model another Session had
|
|
2241
|
+
* just selected (for example a Mimi manager model leaking into its delegated
|
|
2242
|
+
* Work Session).
|
|
2243
|
+
*
|
|
2231
2244
|
* Persists settings.defaults.text (= the connection id / pool key) so the
|
|
2232
2245
|
* next process startup defaults to the same model — without this, switches
|
|
2233
2246
|
* only live in memory and every restart reverts to the previously persisted
|
|
2234
2247
|
* defaults.text.
|
|
2235
2248
|
*/
|
|
2236
2249
|
switchModel(key, opts) {
|
|
2237
|
-
const
|
|
2250
|
+
const perSession = opts?.persist === false;
|
|
2251
|
+
const entry = perSession ? this.modelPool.get(key) : this.modelPool.switch(key);
|
|
2252
|
+
if (!entry) {
|
|
2253
|
+
const available = this.modelPool
|
|
2254
|
+
.list()
|
|
2255
|
+
.map((candidate) => candidate.key)
|
|
2256
|
+
.join(", ");
|
|
2257
|
+
throw new Error(`Model "${key}" not found. Available: ${available}`);
|
|
2258
|
+
}
|
|
2238
2259
|
// LLMConfig is pure model identity now — rotate it wholesale. Cross-model
|
|
2239
2260
|
// runtime knobs (temperature/timeout/retryMaxAttempts/imageDetail) live on
|
|
2240
2261
|
// this.config.clientDefaults and survive the switch untouched.
|
|
@@ -2243,7 +2264,7 @@ export class Engine {
|
|
|
2243
2264
|
// persist: false is the per-session path (ChatSession) — switching one
|
|
2244
2265
|
// session's model must not rewrite settings.defaults.text, the boot
|
|
2245
2266
|
// default every future session inherits.
|
|
2246
|
-
if (
|
|
2267
|
+
if (!perSession)
|
|
2247
2268
|
this.persistActiveModel(entry);
|
|
2248
2269
|
return entry;
|
|
2249
2270
|
}
|
|
@@ -2839,33 +2860,149 @@ export class Engine {
|
|
|
2839
2860
|
return contextManager;
|
|
2840
2861
|
}
|
|
2841
2862
|
/**
|
|
2842
|
-
* Archive a
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
*
|
|
2846
|
-
*
|
|
2847
|
-
*
|
|
2848
|
-
*
|
|
2849
|
-
*
|
|
2850
|
-
|
|
2851
|
-
|
|
2863
|
+
* Archive a contiguous message window of a session into a single anchored
|
|
2864
|
+
* summary, leaving everything outside the window untouched, and cache the
|
|
2865
|
+
* result so a later forceCompact/resume reads the archived history. This is
|
|
2866
|
+
* a generic range-archival facade over ContextManager.summarizeRange.
|
|
2867
|
+
* Returns token stats before/after; equal before/after means the window was
|
|
2868
|
+
* empty/unresolvable or the summary was rejected.
|
|
2869
|
+
*
|
|
2870
|
+
* Window resolution depends on whether the caller supplied anchors:
|
|
2871
|
+
* - With `anchors`, the caller's `range` is IGNORED and the window is
|
|
2872
|
+
* resolved from the client-message-id anchors over a fresh, marker-aware
|
|
2873
|
+
* transcript replay. Callers (the pet segment closure) compute index
|
|
2874
|
+
* ranges over the RAW transcript message list, whose indices grow
|
|
2875
|
+
* forever — but the live list is trimmed by every persisted marker
|
|
2876
|
+
* (replay after restart) and by each in-process archival, so a raw index
|
|
2877
|
+
* range clamps to an empty window or, worse, onto the WRONG tail
|
|
2878
|
+
* messages, which would then be persistently mis-summarized against
|
|
2879
|
+
* correct anchors. Anchors are the only stable coordinates.
|
|
2880
|
+
* - Without `anchors`, legacy behavior: `range` is a half-open index window
|
|
2881
|
+
* over the cached/in-process message list, and nothing is persisted.
|
|
2882
|
+
*/
|
|
2883
|
+
async archiveTurnRange(sessionId, range, anchors) {
|
|
2852
2884
|
const effectiveSessionId = sessionId || this.lastSessionId;
|
|
2853
2885
|
if (!effectiveSessionId)
|
|
2854
2886
|
return { before: 0, after: 0 };
|
|
2855
2887
|
const session = this.sessionManager.resume(effectiveSessionId);
|
|
2856
|
-
|
|
2888
|
+
let sourceMessages;
|
|
2889
|
+
let window;
|
|
2890
|
+
if (anchors) {
|
|
2891
|
+
const { messages: liveMessages, liveIndexByClientMessageId } = session.transcript.toMessagesWithIndex();
|
|
2892
|
+
sourceMessages = liveMessages;
|
|
2893
|
+
// An absent from-anchor means "from the beginning" — over the live
|
|
2894
|
+
// list that is index 0, which includes any previously replayed
|
|
2895
|
+
// from-less summary at the head, so summarizeRange merge-feeds it
|
|
2896
|
+
// (extractAnchoredSummary) instead of losing it.
|
|
2897
|
+
const start = anchors.fromClientMessageId !== undefined
|
|
2898
|
+
? liveIndexByClientMessageId.get(anchors.fromClientMessageId)
|
|
2899
|
+
: 0;
|
|
2900
|
+
const end = liveIndexByClientMessageId.get(anchors.toClientMessageId);
|
|
2901
|
+
if (start === undefined || end === undefined || end <= start) {
|
|
2902
|
+
// Fail open: no summarization, no persistence. Falling back to the
|
|
2903
|
+
// caller's raw range is exactly the bug this path exists to fix —
|
|
2904
|
+
// it would summarize the wrong messages and persist that against
|
|
2905
|
+
// the (correct) anchors, with segmentId dedupe blocking correction.
|
|
2906
|
+
logger.warn("engine.archive_range.anchor_window_unresolved", {
|
|
2907
|
+
sessionId: effectiveSessionId,
|
|
2908
|
+
segmentId: anchors.segmentId,
|
|
2909
|
+
fromClientMessageId: anchors.fromClientMessageId,
|
|
2910
|
+
toClientMessageId: anchors.toClientMessageId,
|
|
2911
|
+
});
|
|
2912
|
+
const before = estimateTokens(sourceMessages);
|
|
2913
|
+
return { before, after: before };
|
|
2914
|
+
}
|
|
2915
|
+
window = { start, end };
|
|
2916
|
+
}
|
|
2917
|
+
else {
|
|
2918
|
+
sourceMessages =
|
|
2919
|
+
this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
|
|
2920
|
+
window = range;
|
|
2921
|
+
}
|
|
2857
2922
|
const before = estimateTokens(sourceMessages);
|
|
2858
2923
|
const contextManager = await this.prepareContextManagerForSession(effectiveSessionId, session, sourceMessages, "archive_range");
|
|
2859
2924
|
// Range archival is initiated deliberately, not by a pressure heuristic;
|
|
2860
2925
|
// don't let a stale run callback retained on lastContextManager double-emit.
|
|
2861
2926
|
contextManager.setOnCompact(() => { });
|
|
2862
|
-
const archived = await contextManager.summarizeRange(sourceMessages,
|
|
2927
|
+
const archived = await contextManager.summarizeRange(sourceMessages, window);
|
|
2863
2928
|
const after = estimateTokens(archived);
|
|
2929
|
+
// Persist the boundary so a restart replays the trimmed context. Only when
|
|
2930
|
+
// summarizeRange actually replaced the span (identity return means empty
|
|
2931
|
+
// window / rejected summary / no summarizer available) and the caller
|
|
2932
|
+
// supplied stable anchors to record it against.
|
|
2933
|
+
if (anchors && archived !== sourceMessages) {
|
|
2934
|
+
const clampedStart = Math.max(0, Math.min(window.start, archived.length - 1));
|
|
2935
|
+
const summaryMessage = archived[clampedStart];
|
|
2936
|
+
const summaryText = typeof summaryMessage?.content === "string" ? summaryMessage.content : undefined;
|
|
2937
|
+
if (summaryText && this.hasLiveArchiveAnchors(session, anchors)) {
|
|
2938
|
+
session.transcript.appendRangeArchive({ summary: summaryText, ...anchors });
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
// In the anchored path this replaces any in-process-only cache state with
|
|
2942
|
+
// the fresh-replay-based result — safe, because the transcript is the
|
|
2943
|
+
// superset of the cache and pressure compaction re-runs if needed.
|
|
2864
2944
|
this.compactedMessagesBySession.set(effectiveSessionId, archived);
|
|
2865
2945
|
this.lastSessionId = effectiveSessionId;
|
|
2866
2946
|
this.lastMessages = archived;
|
|
2867
2947
|
return { before, after };
|
|
2868
2948
|
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Guard against writing a marker anchored to a client message id the
|
|
2951
|
+
* transcript doesn't actually contain. toMessages()'s replay silently
|
|
2952
|
+
* ignores a range_archive event whose anchor doesn't resolve (fail open),
|
|
2953
|
+
* so a bad anchor wouldn't corrupt anything on its own — but if the marker
|
|
2954
|
+
* carries a segmentId, appendRangeArchive's idempotency check treats that
|
|
2955
|
+
* segmentId as "already recorded" forever, with no retry path (e.g. a
|
|
2956
|
+
* one-time migration keyed "migration-v1" would be permanently burned on
|
|
2957
|
+
* its first, failed attempt). Reject before writing so the caller can
|
|
2958
|
+
* retry with a corrected anchor. fromClientMessageId is optional in the
|
|
2959
|
+
* data model (absent means "from the beginning"), so only validate it when
|
|
2960
|
+
* present; missing it is fail-open by design, same as the replay path.
|
|
2961
|
+
*/
|
|
2962
|
+
hasLiveArchiveAnchors(session, anchors) {
|
|
2963
|
+
if (!session.transcript.hasClientMessageId(anchors.toClientMessageId))
|
|
2964
|
+
return false;
|
|
2965
|
+
if (anchors.fromClientMessageId !== undefined &&
|
|
2966
|
+
!session.transcript.hasClientMessageId(anchors.fromClientMessageId)) {
|
|
2967
|
+
return false;
|
|
2968
|
+
}
|
|
2969
|
+
return true;
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Persist an archive boundary WITHOUT a summarization call — the caller
|
|
2973
|
+
* already has the summary text (e.g. the one-time migration built from
|
|
2974
|
+
* pet journal entries). Wraps the plain text in the anchored-summary
|
|
2975
|
+
* envelope so replay and rolling-merge treat it like a real archive.
|
|
2976
|
+
* Returns false when the segmentId was already recorded (idempotent) OR
|
|
2977
|
+
* when the anchors don't resolve to real messages in this transcript —
|
|
2978
|
+
* see hasLiveArchiveAnchors for why a dead anchor must be rejected before
|
|
2979
|
+
* the (potentially one-shot) segmentId gets burned.
|
|
2980
|
+
*/
|
|
2981
|
+
async appendArchiveMarker(sessionId, marker) {
|
|
2982
|
+
const session = this.sessionManager.resume(sessionId);
|
|
2983
|
+
if (!this.hasLiveArchiveAnchors(session, marker)) {
|
|
2984
|
+
logger.warn("engine.archive_marker.dead_anchor", {
|
|
2985
|
+
sessionId,
|
|
2986
|
+
segmentId: marker.segmentId,
|
|
2987
|
+
toClientMessageId: marker.toClientMessageId,
|
|
2988
|
+
fromClientMessageId: marker.fromClientMessageId,
|
|
2989
|
+
});
|
|
2990
|
+
return false;
|
|
2991
|
+
}
|
|
2992
|
+
const wrapped = buildAnchoredSummaryMessage(marker.summary, {
|
|
2993
|
+
...(session.transcript.isPersistent()
|
|
2994
|
+
? { transcriptPath: session.transcript.getFilePath() }
|
|
2995
|
+
: {}),
|
|
2996
|
+
});
|
|
2997
|
+
const content = typeof wrapped.content === "string" ? wrapped.content : marker.summary;
|
|
2998
|
+
const appended = session.transcript.appendRangeArchive({ ...marker, summary: content });
|
|
2999
|
+
if (!appended)
|
|
3000
|
+
return false;
|
|
3001
|
+
// The in-memory cache (if any) predates the marker; drop it so the next
|
|
3002
|
+
// run rebuilds from the trimmed transcript replay.
|
|
3003
|
+
this.compactedMessagesBySession.delete(sessionId);
|
|
3004
|
+
return true;
|
|
3005
|
+
}
|
|
2869
3006
|
recordCacheReadDiagnostics(sessionId, sample) {
|
|
2870
3007
|
const result = this.promptCacheDiagnostics.record(sessionId, sample);
|
|
2871
3008
|
if (result.kind === "scope_changed") {
|
|
@@ -32,6 +32,7 @@ export interface RunPromptComposerConfigInput {
|
|
|
32
32
|
disabledPlugins: ComposerOptions["disabledPlugins"];
|
|
33
33
|
skillAllowlist: ComposerOptions["skillAllowlist"];
|
|
34
34
|
memoriesMaxAgeDays: ComposerOptions["memoriesMaxAgeDays"];
|
|
35
|
+
memoryCurrentProjectOnly?: ComposerOptions["memoryCurrentProjectOnly"];
|
|
35
36
|
goalToolState: ComposerOptions["goalToolState"];
|
|
36
37
|
capabilityPromptSections: ComposerOptions["capabilityPromptSections"];
|
|
37
38
|
dynamicContextProviders: ComposerOptions["dynamicContextProviders"];
|
package/dist/engine/run-setup.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveRunProfileState(args) {
|
|
|
23
23
|
}
|
|
24
24
|
/** Build the prompt-composer options for a run without capturing the Engine facade. */
|
|
25
25
|
export function buildPromptComposerConfig(args) {
|
|
26
|
-
const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
26
|
+
const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
27
27
|
return {
|
|
28
28
|
cwd,
|
|
29
29
|
model,
|
|
@@ -49,6 +49,7 @@ export function buildPromptComposerConfig(args) {
|
|
|
49
49
|
disabledPlugins,
|
|
50
50
|
skillAllowlist,
|
|
51
51
|
memoriesMaxAgeDays,
|
|
52
|
+
memoryCurrentProjectOnly,
|
|
52
53
|
goalToolState,
|
|
53
54
|
capabilityPromptSections,
|
|
54
55
|
dynamicContextProviders,
|
|
@@ -32,6 +32,13 @@ export interface RunBehaviorProfile {
|
|
|
32
32
|
disablePlanMode?: boolean;
|
|
33
33
|
/** When true, MCP servers are neither connected nor exposed for the run. */
|
|
34
34
|
disableMcp?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When true, the injected persistent-memory index is trimmed to this run's
|
|
37
|
+
* project: global-layer project-type / dream-scope records tied to other
|
|
38
|
+
* projects are dropped. Meant for manager-style profiles whose runs never
|
|
39
|
+
* work inside other repos.
|
|
40
|
+
*/
|
|
41
|
+
memoryCurrentProjectOnly?: boolean;
|
|
35
42
|
/**
|
|
36
43
|
* Wrapper tag for host-provided runtime context injected at the system
|
|
37
44
|
* prompt tail (e.g. "pet-world"). Injection happens only when both this tag
|
|
@@ -202,9 +202,6 @@ export declare class TurnLoop {
|
|
|
202
202
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
203
203
|
*/
|
|
204
204
|
private stopBlockCount;
|
|
205
|
-
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
206
|
-
private repeatedToolBatchFingerprint;
|
|
207
|
-
private repeatedToolBatchCount;
|
|
208
205
|
/**
|
|
209
206
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
210
207
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -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 {
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { buildAgentDirectionMessage } from "../tool-system/builtin/agent-notifications.js";
|
|
9
9
|
import { newTurnId } from "./turn-state.js";
|
|
10
10
|
import { formatFriendlyError } from "./friendly-error.js";
|
|
@@ -40,39 +40,6 @@ export function toolResultToBlock(result) {
|
|
|
40
40
|
block.is_error = true;
|
|
41
41
|
return block;
|
|
42
42
|
}
|
|
43
|
-
const REPEATED_TOOL_BATCH_LIMIT = 3;
|
|
44
|
-
function canonicalToolValue(value) {
|
|
45
|
-
if (value === null)
|
|
46
|
-
return "null";
|
|
47
|
-
if (value === undefined)
|
|
48
|
-
return "undefined";
|
|
49
|
-
if (Array.isArray(value))
|
|
50
|
-
return `[${value.map(canonicalToolValue).join(",")}]`;
|
|
51
|
-
if (typeof value === "object") {
|
|
52
|
-
return `{${Object.entries(value)
|
|
53
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
54
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalToolValue(entry)}`)
|
|
55
|
-
.join(",")}}`;
|
|
56
|
-
}
|
|
57
|
-
return JSON.stringify(value);
|
|
58
|
-
}
|
|
59
|
-
function repeatedToolBatchFingerprint(toolCalls, results) {
|
|
60
|
-
// Hash immediately and never log the canonical source: tool results may
|
|
61
|
-
// contain credentials or large media payloads. Call ids are deliberately
|
|
62
|
-
// omitted because providers generate a fresh id for every identical retry.
|
|
63
|
-
return createHash("sha256")
|
|
64
|
-
.update(canonicalToolValue({
|
|
65
|
-
calls: toolCalls.map((call) => ({ toolName: call.toolName, args: call.args })),
|
|
66
|
-
results: results.map((result) => ({
|
|
67
|
-
toolName: result.toolName,
|
|
68
|
-
isError: result.isError === true || Boolean(result.error),
|
|
69
|
-
error: result.error,
|
|
70
|
-
result: result.result,
|
|
71
|
-
contentBlocks: result.contentBlocks,
|
|
72
|
-
})),
|
|
73
|
-
}))
|
|
74
|
-
.digest("hex");
|
|
75
|
-
}
|
|
76
43
|
export class TurnLoop {
|
|
77
44
|
deps;
|
|
78
45
|
config;
|
|
@@ -109,9 +76,6 @@ export class TurnLoop {
|
|
|
109
76
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
110
77
|
*/
|
|
111
78
|
stopBlockCount = 0;
|
|
112
|
-
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
113
|
-
repeatedToolBatchFingerprint;
|
|
114
|
-
repeatedToolBatchCount = 0;
|
|
115
79
|
/**
|
|
116
80
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
117
81
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
|
@@ -1445,49 +1409,6 @@ export class TurnLoop {
|
|
|
1445
1409
|
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
1446
1410
|
}
|
|
1447
1411
|
}
|
|
1448
|
-
const toolBatchFingerprint = repeatedToolBatchFingerprint(toolCalls, results);
|
|
1449
|
-
if (toolBatchFingerprint === this.repeatedToolBatchFingerprint) {
|
|
1450
|
-
this.repeatedToolBatchCount++;
|
|
1451
|
-
}
|
|
1452
|
-
else {
|
|
1453
|
-
this.repeatedToolBatchFingerprint = toolBatchFingerprint;
|
|
1454
|
-
this.repeatedToolBatchCount = 1;
|
|
1455
|
-
}
|
|
1456
|
-
if (this.repeatedToolBatchCount >= REPEATED_TOOL_BATCH_LIMIT) {
|
|
1457
|
-
tlog.warn("turn.repeated_tool_batch_stopped", {
|
|
1458
|
-
cat: "turn",
|
|
1459
|
-
repeatedCount: this.repeatedToolBatchCount,
|
|
1460
|
-
tools: toolCalls.map((call) => call.toolName),
|
|
1461
|
-
});
|
|
1462
|
-
await this.emitHook("on_turn_end", {
|
|
1463
|
-
turnNumber: this.turnCount,
|
|
1464
|
-
hasToolUse: true,
|
|
1465
|
-
toolCallCount: toolCalls.length,
|
|
1466
|
-
});
|
|
1467
|
-
finalText =
|
|
1468
|
-
`检测到同一组工具调用及其结果连续重复 ${REPEATED_TOOL_BATCH_LIMIT} 次,` +
|
|
1469
|
-
"已自动停止,避免继续空转。请调整请求或让 Session 获取新的上下文后再试。";
|
|
1470
|
-
this.deps.transcript.appendMessage("assistant", finalText);
|
|
1471
|
-
messages.push({ role: "assistant", content: finalText });
|
|
1472
|
-
this.config.onStream?.({
|
|
1473
|
-
type: "assistant_message",
|
|
1474
|
-
messageId: assistantMessageId,
|
|
1475
|
-
message: { role: "assistant", content: finalText },
|
|
1476
|
-
});
|
|
1477
|
-
this.finalizeModelTurn();
|
|
1478
|
-
if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
1479
|
-
this.repeatedToolBatchFingerprint = undefined;
|
|
1480
|
-
this.repeatedToolBatchCount = 0;
|
|
1481
|
-
continue;
|
|
1482
|
-
}
|
|
1483
|
-
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1484
|
-
return {
|
|
1485
|
-
text: finalText,
|
|
1486
|
-
reason: "completed",
|
|
1487
|
-
messages,
|
|
1488
|
-
completionKind: "limit_stop",
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
1412
|
// Hook: turn end
|
|
1492
1413
|
await this.emitHook("on_turn_end", {
|
|
1493
1414
|
turnNumber: this.turnCount,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.8.
|
|
6
|
+
export declare const VERSION = "0.8.8";
|
|
7
7
|
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
|
|
9
9
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.8.
|
|
6
|
+
export const VERSION = "0.8.8";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Engine (primary API) ────────────────────────────────────────
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const PANEL_APP_MANIFEST_FILE = ".codeshell-panel/panel.json";
|
|
3
|
-
export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "credentials.cookies", "automations.manage"];
|
|
3
|
+
export declare const PANEL_APP_PERMISSIONS: readonly ["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"];
|
|
4
4
|
export declare const PANEL_APP_ICONS: readonly ["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"];
|
|
5
5
|
export declare const PanelAppAgentTool: z.ZodEffects<z.ZodObject<{
|
|
6
6
|
name: z.ZodString;
|
|
@@ -116,12 +116,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
116
116
|
icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
|
|
117
117
|
placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
|
|
118
118
|
singleton: z.ZodDefault<z.ZodBoolean>;
|
|
119
|
-
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
119
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
120
120
|
schemaVersion: z.ZodLiteral<1>;
|
|
121
121
|
}, "strict", z.ZodTypeAny, {
|
|
122
122
|
id: string;
|
|
123
123
|
version: string;
|
|
124
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
|
|
124
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
125
125
|
entry: string;
|
|
126
126
|
title: {
|
|
127
127
|
default: string;
|
|
@@ -144,7 +144,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
144
144
|
};
|
|
145
145
|
schemaVersion: 1;
|
|
146
146
|
description?: string | undefined;
|
|
147
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
147
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
148
148
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
149
149
|
placement?: "right-dock" | undefined;
|
|
150
150
|
singleton?: boolean | undefined;
|
|
@@ -230,12 +230,12 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
230
230
|
icon: z.ZodDefault<z.ZodEnum<["panel", "activity", "bar-chart-3", "chart", "file-text", "globe", "image", "layout-dashboard", "line-chart", "palette", "pie-chart", "table", "terminal"]>>;
|
|
231
231
|
placement: z.ZodDefault<z.ZodLiteral<"right-dock">>;
|
|
232
232
|
singleton: z.ZodDefault<z.ZodBoolean>;
|
|
233
|
-
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
233
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<["context.session", "context.workspace", "storage", "external.open", "agent.submitPrompt", "workspace.info", "workspace.read", "workspace.write", "notifications.send", "audio.transcribe", "credentials.cookies", "automations.manage"]>, "many">>;
|
|
234
234
|
schemaVersion: z.ZodLiteral<2>;
|
|
235
235
|
}, "strict", z.ZodTypeAny, {
|
|
236
236
|
id: string;
|
|
237
237
|
version: string;
|
|
238
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
|
|
238
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
239
239
|
entry: string;
|
|
240
240
|
title: {
|
|
241
241
|
default: string;
|
|
@@ -276,14 +276,14 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
276
276
|
}[] | undefined;
|
|
277
277
|
} | undefined;
|
|
278
278
|
description?: string | undefined;
|
|
279
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
279
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
280
280
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
281
281
|
placement?: "right-dock" | undefined;
|
|
282
282
|
singleton?: boolean | undefined;
|
|
283
283
|
}>]>, {
|
|
284
284
|
id: string;
|
|
285
285
|
version: string;
|
|
286
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
|
|
286
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
287
287
|
entry: string;
|
|
288
288
|
title: {
|
|
289
289
|
default: string;
|
|
@@ -298,7 +298,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
298
298
|
} | {
|
|
299
299
|
id: string;
|
|
300
300
|
version: string;
|
|
301
|
-
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[];
|
|
301
|
+
permissions: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[];
|
|
302
302
|
entry: string;
|
|
303
303
|
title: {
|
|
304
304
|
default: string;
|
|
@@ -330,7 +330,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
330
330
|
};
|
|
331
331
|
schemaVersion: 1;
|
|
332
332
|
description?: string | undefined;
|
|
333
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
333
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
334
334
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
335
335
|
placement?: "right-dock" | undefined;
|
|
336
336
|
singleton?: boolean | undefined;
|
|
@@ -354,7 +354,7 @@ export declare const PanelAppManifest: z.ZodEffects<z.ZodDiscriminatedUnion<"sch
|
|
|
354
354
|
}[] | undefined;
|
|
355
355
|
} | undefined;
|
|
356
356
|
description?: string | undefined;
|
|
357
|
-
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
357
|
+
permissions?: ("context.session" | "context.workspace" | "storage" | "external.open" | "agent.submitPrompt" | "workspace.info" | "workspace.read" | "workspace.write" | "notifications.send" | "audio.transcribe" | "credentials.cookies" | "automations.manage")[] | undefined;
|
|
358
358
|
icon?: "terminal" | "image" | "panel" | "activity" | "bar-chart-3" | "chart" | "file-text" | "globe" | "layout-dashboard" | "line-chart" | "palette" | "pie-chart" | "table" | undefined;
|
|
359
359
|
placement?: "right-dock" | undefined;
|
|
360
360
|
singleton?: boolean | undefined;
|
|
@@ -11,6 +11,7 @@ export const PANEL_APP_PERMISSIONS = [
|
|
|
11
11
|
"workspace.read",
|
|
12
12
|
"workspace.write",
|
|
13
13
|
"notifications.send",
|
|
14
|
+
"audio.transcribe",
|
|
14
15
|
"credentials.cookies",
|
|
15
16
|
"automations.manage",
|
|
16
17
|
];
|
|
@@ -180,12 +181,13 @@ export const PanelAppManifest = z
|
|
|
180
181
|
});
|
|
181
182
|
}
|
|
182
183
|
if ((value.permissions.includes("workspace.read") ||
|
|
183
|
-
value.permissions.includes("workspace.write")
|
|
184
|
+
value.permissions.includes("workspace.write") ||
|
|
185
|
+
value.permissions.includes("audio.transcribe")) &&
|
|
184
186
|
!value.permissions.includes("context.workspace")) {
|
|
185
187
|
ctx.addIssue({
|
|
186
188
|
code: z.ZodIssueCode.custom,
|
|
187
189
|
path: ["permissions"],
|
|
188
|
-
message: "workspace.read
|
|
190
|
+
message: "workspace.read, workspace.write, and audio.transcribe require context.workspace",
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
193
|
if (value.permissions.includes("automations.manage") &&
|
|
@@ -89,6 +89,13 @@ export interface ComposerOptions {
|
|
|
89
89
|
* Undefined/0 → inject all.
|
|
90
90
|
*/
|
|
91
91
|
memoriesMaxAgeDays?: number;
|
|
92
|
+
/**
|
|
93
|
+
* When true (set from the active behavior profile, e.g. the Pet manager),
|
|
94
|
+
* the injected memory index keeps only memories relevant to this cwd —
|
|
95
|
+
* global-layer project/dream records tied to other projects are dropped.
|
|
96
|
+
* See MemoryManager.buildInjectionIndex(currentProjectOnly).
|
|
97
|
+
*/
|
|
98
|
+
memoryCurrentProjectOnly?: boolean;
|
|
92
99
|
}
|
|
93
100
|
export declare class PromptComposer {
|
|
94
101
|
private readonly options;
|
package/dist/prompt/composer.js
CHANGED
|
@@ -102,18 +102,24 @@ export class PromptComposer {
|
|
|
102
102
|
});
|
|
103
103
|
const skillsListing = buildSkillListing(skills);
|
|
104
104
|
const declaredSkillGap = this.buildDeclaredSkillGap(skills);
|
|
105
|
-
|
|
105
|
+
// Capability and sources context are independent I/O — resolve them
|
|
106
|
+
// concurrently instead of serially.
|
|
107
|
+
const [capabilityContext, sourcesContext] = await Promise.all([
|
|
108
|
+
this.buildSystemContext(),
|
|
109
|
+
(async () => {
|
|
110
|
+
try {
|
|
111
|
+
return (await this.options.sourcesContextProvider?.()) ?? "";
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Optional metadata context must not make a turn fail.
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
})(),
|
|
118
|
+
]);
|
|
106
119
|
// Memory rides here (tail, past the cache breakpoint) — not the system
|
|
107
120
|
// prefix — so a memory change (extraction / recall usage++ / approve) never
|
|
108
121
|
// re-bills the cached prefix. See buildUserContextMessage for the rationale.
|
|
109
122
|
const memoryContext = this.getMemoryContext();
|
|
110
|
-
let sourcesContext = "";
|
|
111
|
-
try {
|
|
112
|
-
sourcesContext = (await this.options.sourcesContextProvider?.()) ?? "";
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// Optional metadata context must not make a turn fail.
|
|
116
|
-
}
|
|
117
123
|
const goalToolContext = this.buildGoalToolContext();
|
|
118
124
|
const parts = [
|
|
119
125
|
skillsListing,
|
|
@@ -268,6 +274,7 @@ export class PromptComposer {
|
|
|
268
274
|
projectDir: this.options.cwd,
|
|
269
275
|
profileDir: this.options.profileMemoryDir,
|
|
270
276
|
maxAgeDays: this.options.memoriesMaxAgeDays,
|
|
277
|
+
currentProjectOnly: this.options.memoryCurrentProjectOnly,
|
|
271
278
|
});
|
|
272
279
|
}
|
|
273
280
|
catch {
|
package/dist/protocol/server.js
CHANGED
|
@@ -2240,7 +2240,17 @@ export class AgentServer {
|
|
|
2240
2240
|
if (!archiveEngine)
|
|
2241
2241
|
return;
|
|
2242
2242
|
try {
|
|
2243
|
-
const
|
|
2243
|
+
const toClientMessageId = typeof params.toClientMessageId === "string" ? params.toClientMessageId : undefined;
|
|
2244
|
+
const anchors = toClientMessageId
|
|
2245
|
+
? {
|
|
2246
|
+
toClientMessageId,
|
|
2247
|
+
...(typeof params.fromClientMessageId === "string"
|
|
2248
|
+
? { fromClientMessageId: params.fromClientMessageId }
|
|
2249
|
+
: {}),
|
|
2250
|
+
...(typeof params.segmentId === "string" ? { segmentId: params.segmentId } : {}),
|
|
2251
|
+
}
|
|
2252
|
+
: undefined;
|
|
2253
|
+
const result = await archiveEngine.archiveTurnRange(archiveSessionId, { start, end }, anchors);
|
|
2244
2254
|
if (result.before > result.after) {
|
|
2245
2255
|
const event = {
|
|
2246
2256
|
type: "context_compact",
|
|
@@ -2265,6 +2275,35 @@ export class AgentServer {
|
|
|
2265
2275
|
}
|
|
2266
2276
|
break;
|
|
2267
2277
|
}
|
|
2278
|
+
case "archive_marker": {
|
|
2279
|
+
const markerSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
2280
|
+
? params.sessionId
|
|
2281
|
+
: undefined;
|
|
2282
|
+
const summary = typeof params.summary === "string" ? params.summary : undefined;
|
|
2283
|
+
const toClientMessageId = typeof params.toClientMessageId === "string" ? params.toClientMessageId : undefined;
|
|
2284
|
+
if (!markerSessionId || !summary || !toClientMessageId) {
|
|
2285
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "archive_marker requires sessionId, summary and toClientMessageId"));
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const markerEngine = await this.resolveEngineForSessionQuery(req, markerSessionId, engine, "archive_marker");
|
|
2289
|
+
if (!markerEngine)
|
|
2290
|
+
return;
|
|
2291
|
+
try {
|
|
2292
|
+
const appended = await markerEngine.appendArchiveMarker(markerSessionId, {
|
|
2293
|
+
summary,
|
|
2294
|
+
toClientMessageId,
|
|
2295
|
+
...(typeof params.fromClientMessageId === "string"
|
|
2296
|
+
? { fromClientMessageId: params.fromClientMessageId }
|
|
2297
|
+
: {}),
|
|
2298
|
+
...(typeof params.segmentId === "string" ? { segmentId: params.segmentId } : {}),
|
|
2299
|
+
});
|
|
2300
|
+
this.transport.send(createResponse(req.id, { type: "archive_marker", data: { appended } }));
|
|
2301
|
+
}
|
|
2302
|
+
catch (err) {
|
|
2303
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
2304
|
+
}
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2268
2307
|
case "models": {
|
|
2269
2308
|
if (!engine) {
|
|
2270
2309
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for models query"));
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -342,6 +342,20 @@ export interface QueryParams {
|
|
|
342
342
|
start?: unknown;
|
|
343
343
|
/** Used by archive_range: half-open message-index window end (exclusive). */
|
|
344
344
|
end?: unknown;
|
|
345
|
+
/**
|
|
346
|
+
* Used by archive_range (optional) and archive_marker (required): the
|
|
347
|
+
* clientMessageId the archived/marked span ends at. archive_range only
|
|
348
|
+
* persists a boundary when this anchor is supplied and resolves to a live
|
|
349
|
+
* message; without it, archive_range behaves exactly as before (in-memory
|
|
350
|
+
* compaction only, no persisted range_archive event).
|
|
351
|
+
*/
|
|
352
|
+
toClientMessageId?: string;
|
|
353
|
+
/** Used by archive_range / archive_marker: optional start-of-span anchor. */
|
|
354
|
+
fromClientMessageId?: string;
|
|
355
|
+
/** Used by archive_range / archive_marker: optional idempotency key. */
|
|
356
|
+
segmentId?: string;
|
|
357
|
+
/** Used by archive_marker: the summary text to persist without a model call. */
|
|
358
|
+
summary?: string;
|
|
345
359
|
}
|
|
346
360
|
export interface QueryResult {
|
|
347
361
|
type: string;
|
package/dist/session/memory.d.ts
CHANGED
|
@@ -233,6 +233,16 @@ export declare class MemoryManager {
|
|
|
233
233
|
baseDir?: string;
|
|
234
234
|
maxAgeDays?: number;
|
|
235
235
|
now?: number;
|
|
236
|
+
/**
|
|
237
|
+
* Trim the global layer to what is relevant to this run's project:
|
|
238
|
+
* project-type and dream-scope records are per-project experience, so
|
|
239
|
+
* they are kept only when their origin metadata ties them to
|
|
240
|
+
* `projectDir`. General knowledge (user/feedback/reference in user
|
|
241
|
+
* scope) always stays; the project layer is untouched because it is
|
|
242
|
+
* already this project's own store. Set by behavior profiles whose runs
|
|
243
|
+
* never work inside other repos (e.g. a manager/dispatcher session).
|
|
244
|
+
*/
|
|
245
|
+
currentProjectOnly?: boolean;
|
|
236
246
|
}): string;
|
|
237
247
|
/**
|
|
238
248
|
* Load every entry belonging to the given scope, without changing the
|
package/dist/session/memory.js
CHANGED
|
@@ -651,7 +651,13 @@ export class MemoryManager {
|
|
|
651
651
|
const profile = opts.profileDir ? new MemoryManager({ baseDir: opts.profileDir }) : null;
|
|
652
652
|
const pinnedFirst = (a, b) => Number(b.pinned ?? false) - Number(a.pinned ?? false);
|
|
653
653
|
const collect = (mm) => filterByAge([...mm.loadScope("user"), ...mm.loadScope("dream")], opts.maxAgeDays, opts.now).sort(pinnedFirst);
|
|
654
|
-
|
|
654
|
+
let globalEntries = collect(global);
|
|
655
|
+
if (opts.currentProjectOnly) {
|
|
656
|
+
const tiedToProject = (e) => opts.projectDir !== undefined &&
|
|
657
|
+
(e.originProject === opts.projectDir ||
|
|
658
|
+
(e.originProjects?.includes(opts.projectDir) ?? false));
|
|
659
|
+
globalEntries = globalEntries.filter((e) => (e.type !== "project" && e.scope !== "dream") || tiedToProject(e));
|
|
660
|
+
}
|
|
655
661
|
const profileEntries = profile ? collect(profile) : [];
|
|
656
662
|
const projectEntries = project ? collect(project) : [];
|
|
657
663
|
if (globalEntries.length === 0 && profileEntries.length === 0 && projectEntries.length === 0) {
|
|
@@ -106,12 +106,48 @@ export declare class Transcript {
|
|
|
106
106
|
*/
|
|
107
107
|
appendTurnStopped(): TranscriptEvent | undefined;
|
|
108
108
|
appendSummary(summary: string, metadata: SummaryAppendMetadata): TranscriptEvent;
|
|
109
|
+
/**
|
|
110
|
+
* Persist a range-archival boundary. Span is [fromClientMessageId,
|
|
111
|
+
* toClientMessageId) over message events; an absent from means "from the
|
|
112
|
+
* beginning". Idempotent on segmentId so a crash-replayed closure cannot
|
|
113
|
+
* double-archive.
|
|
114
|
+
*/
|
|
115
|
+
appendRangeArchive(data: {
|
|
116
|
+
summary: string;
|
|
117
|
+
toClientMessageId: string;
|
|
118
|
+
fromClientMessageId?: string;
|
|
119
|
+
segmentId?: string;
|
|
120
|
+
}): TranscriptEvent | undefined;
|
|
109
121
|
appendError(error: string, details?: Record<string, unknown>): TranscriptEvent;
|
|
110
122
|
/**
|
|
111
123
|
* Derive Message[] from transcript events for sending to the LLM.
|
|
112
124
|
* This is the critical boundary: the LLM never sees the event log directly.
|
|
113
125
|
*/
|
|
114
126
|
toMessages(): Message[];
|
|
127
|
+
/**
|
|
128
|
+
* Marker-aware replay that ALSO reports, for every emitted message event
|
|
129
|
+
* carrying a clientMessageId, the LIVE index (its position in the returned
|
|
130
|
+
* messages array) of its FIRST emission. This is how the engine resolves an
|
|
131
|
+
* anchored archival window: raw transcript indices grow forever and go stale
|
|
132
|
+
* the moment a range_archive marker trims the replay, so any caller-held
|
|
133
|
+
* index range is meaningless — only client-message-id anchors resolved over
|
|
134
|
+
* THIS replay identify the right span.
|
|
135
|
+
*
|
|
136
|
+
* Contract details:
|
|
137
|
+
* - Messages dropped inside an archived span get NO index entry — they are
|
|
138
|
+
* not in the live list, so a window anchored on them cannot be built
|
|
139
|
+
* (the engine fails open in that case).
|
|
140
|
+
* - Only the FIRST emission of a duplicated clientMessageId is recorded
|
|
141
|
+
* (duplicates replay as plain messages per the one-shot span rule; the
|
|
142
|
+
* first index is the meaningful one).
|
|
143
|
+
* - The clientMessageId is deliberately NOT attached to the Message objects
|
|
144
|
+
* themselves: Message[] is exactly what gets serialized into LLM request
|
|
145
|
+
* payloads, and transport metadata must not leak into them.
|
|
146
|
+
*/
|
|
147
|
+
toMessagesWithIndex(): {
|
|
148
|
+
messages: Message[];
|
|
149
|
+
liveIndexByClientMessageId: Map<string, number>;
|
|
150
|
+
};
|
|
115
151
|
getEvents(type?: TranscriptEventType): TranscriptEvent[];
|
|
116
152
|
get turnNumber(): number;
|
|
117
153
|
get eventCount(): number;
|
|
@@ -12,6 +12,7 @@ const CONTEXT_EVENT_TYPES = new Set([
|
|
|
12
12
|
"tool_result",
|
|
13
13
|
"summary",
|
|
14
14
|
"context_transfer",
|
|
15
|
+
"range_archive",
|
|
15
16
|
]);
|
|
16
17
|
const INTERRUPTED_TOOL_RESULT_ERROR = "[Tool result missing due to interrupted session]";
|
|
17
18
|
function isSyntheticInterruptedToolResult(event) {
|
|
@@ -212,6 +213,19 @@ export class Transcript {
|
|
|
212
213
|
},
|
|
213
214
|
});
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Persist a range-archival boundary. Span is [fromClientMessageId,
|
|
218
|
+
* toClientMessageId) over message events; an absent from means "from the
|
|
219
|
+
* beginning". Idempotent on segmentId so a crash-replayed closure cannot
|
|
220
|
+
* double-archive.
|
|
221
|
+
*/
|
|
222
|
+
appendRangeArchive(data) {
|
|
223
|
+
if (data.segmentId &&
|
|
224
|
+
this.events.some((e) => e.type === "range_archive" && e.data.segmentId === data.segmentId)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return this.append("range_archive", { ...data });
|
|
228
|
+
}
|
|
215
229
|
appendError(error, details) {
|
|
216
230
|
return this.append("error", { error, ...details });
|
|
217
231
|
}
|
|
@@ -220,12 +234,128 @@ export class Transcript {
|
|
|
220
234
|
* This is the critical boundary: the LLM never sees the event log directly.
|
|
221
235
|
*/
|
|
222
236
|
toMessages() {
|
|
237
|
+
return this.toMessagesWithIndex().messages;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Marker-aware replay that ALSO reports, for every emitted message event
|
|
241
|
+
* carrying a clientMessageId, the LIVE index (its position in the returned
|
|
242
|
+
* messages array) of its FIRST emission. This is how the engine resolves an
|
|
243
|
+
* anchored archival window: raw transcript indices grow forever and go stale
|
|
244
|
+
* the moment a range_archive marker trims the replay, so any caller-held
|
|
245
|
+
* index range is meaningless — only client-message-id anchors resolved over
|
|
246
|
+
* THIS replay identify the right span.
|
|
247
|
+
*
|
|
248
|
+
* Contract details:
|
|
249
|
+
* - Messages dropped inside an archived span get NO index entry — they are
|
|
250
|
+
* not in the live list, so a window anchored on them cannot be built
|
|
251
|
+
* (the engine fails open in that case).
|
|
252
|
+
* - Only the FIRST emission of a duplicated clientMessageId is recorded
|
|
253
|
+
* (duplicates replay as plain messages per the one-shot span rule; the
|
|
254
|
+
* first index is the meaningful one).
|
|
255
|
+
* - The clientMessageId is deliberately NOT attached to the Message objects
|
|
256
|
+
* themselves: Message[] is exactly what gets serialized into LLM request
|
|
257
|
+
* payloads, and transport metadata must not leak into them.
|
|
258
|
+
*/
|
|
259
|
+
toMessagesWithIndex() {
|
|
223
260
|
const messages = [];
|
|
261
|
+
const liveIndexByClientMessageId = new Map();
|
|
224
262
|
const selectedToolResults = preferredToolResults(this.events);
|
|
263
|
+
const hasRangeArchive = this.events.some((e) => e.type === "range_archive");
|
|
264
|
+
const spansByFromId = new Map();
|
|
265
|
+
let openingSpan;
|
|
266
|
+
if (hasRangeArchive) {
|
|
267
|
+
// First-occurrence event index per client message id, so a marker whose
|
|
268
|
+
// `to` does not come strictly after its `from` (out of order, or a
|
|
269
|
+
// degenerate from === to) can be rejected. Without this check the span
|
|
270
|
+
// opens at `from` but its close condition (`to`) was already passed
|
|
271
|
+
// while scanning forward, so it would never close — silently swallowing
|
|
272
|
+
// the rest of the conversation. Fail open instead: ignore the marker.
|
|
273
|
+
const firstIndexByClientId = new Map();
|
|
274
|
+
for (const [index, event] of this.events.entries()) {
|
|
275
|
+
if (event.type === "message" && typeof event.data.clientMessageId === "string") {
|
|
276
|
+
if (!firstIndexByClientId.has(event.data.clientMessageId)) {
|
|
277
|
+
firstIndexByClientId.set(event.data.clientMessageId, index);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const presentClientIds = new Set(firstIndexByClientId.keys());
|
|
282
|
+
for (const event of this.events) {
|
|
283
|
+
if (event.type !== "range_archive")
|
|
284
|
+
continue;
|
|
285
|
+
const { summary, toClientMessageId, fromClientMessageId } = event.data;
|
|
286
|
+
if (typeof summary !== "string" || !presentClientIds.has(toClientMessageId))
|
|
287
|
+
continue;
|
|
288
|
+
if (fromClientMessageId === undefined) {
|
|
289
|
+
// Multiple from-less markers compete for this single opening-span
|
|
290
|
+
// slot; the LAST one wins (matching spansByFromId's Map.set
|
|
291
|
+
// semantics below). Last-wins is CORRECT by construction, not
|
|
292
|
+
// merely a tiebreak: the engine resolves a from-less archival
|
|
293
|
+
// window as [0, to) over the LIVE replay, so the window that
|
|
294
|
+
// produced a LATER from-less marker began with the EARLIER
|
|
295
|
+
// marker's replayed summary message, and summarizeRange merge-fed
|
|
296
|
+
// that prior summary (extractAnchoredSummary) into the new one.
|
|
297
|
+
// The later summary therefore already contains the earlier one's
|
|
298
|
+
// content — dropping the earlier marker here loses nothing. (And
|
|
299
|
+
// in production from-less windows only advance: each new marker
|
|
300
|
+
// ends at a later boundary, so the surviving span is the widest.)
|
|
301
|
+
openingSpan = { summary, toClientMessageId };
|
|
302
|
+
}
|
|
303
|
+
else if (presentClientIds.has(fromClientMessageId)) {
|
|
304
|
+
const fromIndex = firstIndexByClientId.get(fromClientMessageId);
|
|
305
|
+
const toIndex = firstIndexByClientId.get(toClientMessageId);
|
|
306
|
+
if (toIndex <= fromIndex)
|
|
307
|
+
continue; // out of order or degenerate: fail open
|
|
308
|
+
spansByFromId.set(fromClientMessageId, { summary, toClientMessageId });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
let activeSpan = null;
|
|
313
|
+
if (openingSpan) {
|
|
314
|
+
activeSpan = openingSpan;
|
|
315
|
+
messages.push({ role: "user", content: openingSpan.summary });
|
|
316
|
+
}
|
|
317
|
+
// tool_use ids actually emitted into assistant message content blocks so
|
|
318
|
+
// far. A tool_result whose tool_use_id isn't in this set — e.g. because
|
|
319
|
+
// its opening tool_use fell inside an archived span while the (later,
|
|
320
|
+
// preferred) real result landed outside it — would be an orphaned block
|
|
321
|
+
// that breaks provider validation; skip it instead of emitting it.
|
|
322
|
+
const emittedToolUseIds = new Set();
|
|
225
323
|
for (const event of this.events) {
|
|
324
|
+
// Span bookkeeping runs on message events only: exit before entry so
|
|
325
|
+
// adjacent spans (A.to === B.from) hand over on the boundary message.
|
|
326
|
+
if (event.type === "message") {
|
|
327
|
+
const clientMessageId = typeof event.data.clientMessageId === "string" ? event.data.clientMessageId : undefined;
|
|
328
|
+
if (activeSpan && clientMessageId === activeSpan.toClientMessageId) {
|
|
329
|
+
activeSpan = null;
|
|
330
|
+
}
|
|
331
|
+
if (!activeSpan && clientMessageId && spansByFromId.has(clientMessageId)) {
|
|
332
|
+
activeSpan = spansByFromId.get(clientMessageId);
|
|
333
|
+
// One-shot: a duplicate `from` message (e.g. from a torn JSONL
|
|
334
|
+
// reload) must not reopen this span a second time — it would have
|
|
335
|
+
// no more `to` ahead of it and swallow the rest of the transcript.
|
|
336
|
+
spansByFromId.delete(clientMessageId);
|
|
337
|
+
messages.push({ role: "user", content: activeSpan.summary });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (activeSpan)
|
|
341
|
+
continue; // archived span: drop every context event inside
|
|
226
342
|
switch (event.type) {
|
|
227
343
|
case "message": {
|
|
228
|
-
const { role, content } = event.data;
|
|
344
|
+
const { role, content, clientMessageId } = event.data;
|
|
345
|
+
// Record the live index of this message's FIRST emission before
|
|
346
|
+
// pushing it (the index it is about to occupy). Dropped-in-span
|
|
347
|
+
// messages never reach this point, so they get no entry.
|
|
348
|
+
if (typeof clientMessageId === "string" &&
|
|
349
|
+
!liveIndexByClientMessageId.has(clientMessageId)) {
|
|
350
|
+
liveIndexByClientMessageId.set(clientMessageId, messages.length);
|
|
351
|
+
}
|
|
352
|
+
if (role === "assistant" && Array.isArray(content)) {
|
|
353
|
+
for (const block of content) {
|
|
354
|
+
if (block.type === "tool_use" && typeof block.id === "string") {
|
|
355
|
+
emittedToolUseIds.add(block.id);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
229
359
|
messages.push({ role: role, content });
|
|
230
360
|
break;
|
|
231
361
|
}
|
|
@@ -237,7 +367,8 @@ export class Transcript {
|
|
|
237
367
|
case "tool_result": {
|
|
238
368
|
const eventToolCallId = event.data.toolCallId;
|
|
239
369
|
if (typeof eventToolCallId !== "string" ||
|
|
240
|
-
selectedToolResults.get(eventToolCallId) !== event
|
|
370
|
+
selectedToolResults.get(eventToolCallId) !== event ||
|
|
371
|
+
!emittedToolUseIds.has(eventToolCallId)) {
|
|
241
372
|
break;
|
|
242
373
|
}
|
|
243
374
|
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
@@ -280,10 +411,12 @@ export class Transcript {
|
|
|
280
411
|
break;
|
|
281
412
|
}
|
|
282
413
|
// turn_boundary, run_result, session_meta, file_history, plan_operation, error
|
|
283
|
-
// are not included in LLM messages
|
|
414
|
+
// are not included in LLM messages. range_archive is handled entirely
|
|
415
|
+
// by the pre-pass above (it emits the summary at span entry and drops
|
|
416
|
+
// events inside the span); it never falls through to this switch.
|
|
284
417
|
}
|
|
285
418
|
}
|
|
286
|
-
return messages;
|
|
419
|
+
return { messages, liveIndexByClientMessageId };
|
|
287
420
|
}
|
|
288
421
|
getEvents(type) {
|
|
289
422
|
if (!type)
|
|
@@ -448,6 +581,18 @@ export class Transcript {
|
|
|
448
581
|
});
|
|
449
582
|
break;
|
|
450
583
|
}
|
|
584
|
+
case "range_archive": {
|
|
585
|
+
// A hand-picked context range is an explicit user selection: inject
|
|
586
|
+
// the archive summary as context but do NOT replace/drop the
|
|
587
|
+
// messages inside its span the way toMessages() does — the caller
|
|
588
|
+
// asked for exactly this range and expects to see it in full.
|
|
589
|
+
const { summary } = event.data;
|
|
590
|
+
messages.push({
|
|
591
|
+
role: "user",
|
|
592
|
+
content: `<system-reminder>Archived summary for part of this range:\n${summary}</system-reminder>`,
|
|
593
|
+
});
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
451
596
|
}
|
|
452
597
|
}
|
|
453
598
|
return messages;
|
package/dist/types.d.ts
CHANGED
|
@@ -145,7 +145,7 @@ export interface RegisteredTool {
|
|
|
145
145
|
*/
|
|
146
146
|
timeoutMs?: number;
|
|
147
147
|
}
|
|
148
|
-
export type TranscriptEventType = "message" | "tool_use" | "tool_result" | "summary" | "context_transfer" | "content_replace" | "file_history" | "plan_operation" | "session_meta" | "subagent" | "external_file_changes" | "turn_boundary" | "run_result" | "goal_progress" | "turn_stopped" | "error";
|
|
148
|
+
export type TranscriptEventType = "message" | "tool_use" | "tool_result" | "summary" | "context_transfer" | "range_archive" | "content_replace" | "file_history" | "plan_operation" | "session_meta" | "subagent" | "external_file_changes" | "turn_boundary" | "run_result" | "goal_progress" | "turn_stopped" | "error";
|
|
149
149
|
export interface TranscriptEvent {
|
|
150
150
|
id: string;
|
|
151
151
|
type: TranscriptEventType;
|
package/package.json
CHANGED