newmark-agent 0.6.3 → 0.6.4

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.
@@ -126966,6 +126966,23 @@ var SkillsManager = class {
126966
126966
  // src/core/agent.ts
126967
126967
  init_flow();
126968
126968
 
126969
+ // src/core/branchIdentity.ts
126970
+ var BRANCH_IDENTITY_SEPARATOR = "::branch:";
126971
+ function branchConversationIdentity(conversationId, branchNodeId) {
126972
+ const conversation = String(conversationId || "").trim() || "default";
126973
+ const branch = String(branchNodeId || "").trim();
126974
+ return branch ? `${conversation}${BRANCH_IDENTITY_SEPARATOR}${branch}` : conversation;
126975
+ }
126976
+ function assertFreshBranchIdentity(conversationId, branchNodeId, existing) {
126977
+ const branch = String(branchNodeId || "").trim();
126978
+ if (!branch) throw new Error("Branch conversation identity requires a branch node id.");
126979
+ for (const known of existing) {
126980
+ if (String(known || "") === branch) {
126981
+ throw new Error(`Branch conversation identity ${branchConversationIdentity(conversationId, branch)} is already in use.`);
126982
+ }
126983
+ }
126984
+ }
126985
+
126969
126986
  // src/core/agentKernelRunner.ts
126970
126987
  var fs19 = __toESM(require("fs"));
126971
126988
  var path21 = __toESM(require("path"));
@@ -128046,10 +128063,16 @@ var HIDDEN_LINE_PREFIXES = [
128046
128063
  ];
128047
128064
  function providerSessionIdentity(agent) {
128048
128065
  const conversationId = String(agent.activeConversationId || "default");
128066
+ let runBranchId = "";
128067
+ try {
128068
+ runBranchId = String(agent.activeWorkRunBranchId() || "");
128069
+ } catch {
128070
+ runBranchId = "";
128071
+ }
128072
+ if (runBranchId) return branchConversationIdentity(conversationId, runBranchId);
128049
128073
  try {
128050
128074
  const snapshot2 = agent.getConversationSnapshot(conversationId);
128051
- const branchId = String(snapshot2.runtimeBranchId || snapshot2.activeBranchId || "");
128052
- return branchId ? `${conversationId}::branch:${branchId}` : conversationId;
128075
+ return branchConversationIdentity(conversationId, String(snapshot2.runtimeBranchId || snapshot2.activeBranchId || ""));
128053
128076
  } catch {
128054
128077
  return conversationId;
128055
128078
  }
@@ -132797,6 +132820,22 @@ var Agent5 = class _Agent {
132797
132820
  currentWorkRunId() {
132798
132821
  return this.activeWorkRunId || this.finalizingWorkRunId || "";
132799
132822
  }
132823
+ /**
132824
+ * The branch node that owns the currently active Build. Provider session ids
132825
+ * and branch-scoped caches must use this identity instead of "the branch that
132826
+ * happens to be active", otherwise a concurrently running sibling branch
132827
+ * (branch-communication mode) or a page switched mid-Build would read another
132828
+ * branch's remote session and folded history.
132829
+ */
132830
+ activeWorkRunBranchId() {
132831
+ const runId = this.currentWorkRunId();
132832
+ const run = runId ? this.workRuns.find((item) => String(item.runId || "") === runId) : void 0;
132833
+ return String(run?.branchNodeId || "") || this.currentBranchNodeId();
132834
+ }
132835
+ /** Full conversation identity of one branch node (shared by every branch kind). */
132836
+ branchConversationIdentity(branchNodeId) {
132837
+ return branchConversationIdentity(this.activeConversationId || "default", branchNodeId);
132838
+ }
132800
132839
  activeModelConfig() {
132801
132840
  const deployment = this.activeDeployment();
132802
132841
  return deployment ? this.config.findDeployment(deployment) : this.config.findModel(this.model);
@@ -135035,6 +135074,7 @@ ${next2.body}`;
135035
135074
  });
135036
135075
  const reusableGroup = Object.values(tree.branchGroups).find((group) => group.sourceNodeId === parentNodeId && (targetGuideNodeId && group.sourceGuideId === targetGuideNodeId || !targetGuideNodeId && targetMessageId && group.sourceMessageId === targetMessageId || !group.sourceGuideId && !group.sourceMessageId && group.sourceMessageIndex === index2) || group.nodeIds.includes(parentNodeId) && group.sourceMessageIndex === index2 && (targetGuideNodeId && group.pageGuideIds?.[parentNodeId] === targetGuideNodeId || !targetGuideNodeId && targetMessageId && group.pageMessageIds?.[parentNodeId] === targetMessageId));
135037
135076
  const branchId = crypto15.randomUUID();
135077
+ assertFreshBranchIdentity(clean || this.activeConversationId || "default", branchId, Object.keys(tree.nodes));
135038
135078
  const branch = this.treeNodeFromEntry(branchId, parentNodeId, index2, text3, {
135039
135079
  ...entry,
135040
135080
  chatMessages: sourceNode.chatMessages.slice(0, index2),
@@ -135619,7 +135659,7 @@ Conversation title (a few words):`;
135619
135659
  chatMessages: [...this.chatMessages],
135620
135660
  history: [...this.history],
135621
135661
  providerUsage: this.conversationProviderUsage(),
135622
- compressionCache: [...this.compressionCache],
135662
+ compressionCache: this.mergedCompressionCacheForPersist(stored.conversations[stateKey2]),
135623
135663
  branchMailbox: [...this.branchMailbox],
135624
135664
  branchCommunication: this.branchCommunicationEnabled,
135625
135665
  plan: this.normalizeConversationPlan(this.conversationPlan),
@@ -135677,7 +135717,7 @@ Conversation title (a few words):`;
135677
135717
  if (saved) {
135678
135718
  this.restoreProviderUsage(saved.providerUsage);
135679
135719
  this.history = [...saved.history];
135680
- this.compressionCache = saved.compressionCache ? saved.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
135720
+ this.compressionCache = this.compressionCacheForBranch(saved.compressionCache);
135681
135721
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
135682
135722
  this.branchMailbox = (saved.branchMailbox || []).map((message) => ({ ...message }));
135683
135723
  this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map((message) => Number(message.sequence) || 0)) + 1;
@@ -135706,7 +135746,7 @@ Conversation title (a few words):`;
135706
135746
  this.restoreProviderUsage(persisted?.providerUsage);
135707
135747
  const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
135708
135748
  this.history = repairedHistory.messages;
135709
- this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
135749
+ this.compressionCache = this.compressionCacheForBranch(persisted?.compressionCache);
135710
135750
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
135711
135751
  this.branchMailbox = (persisted?.branchMailbox || []).map((message) => ({ ...message }));
135712
135752
  this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map((message) => Number(message.sequence) || 0)) + 1;
@@ -140558,7 +140598,36 @@ ${text3.slice(-tailChars).trimStart()}`;
140558
140598
  }
140559
140599
  compressionArchiveScopeKey() {
140560
140600
  if (this.isSubagentRuntime || !this.config.getBool("context", "compression_archive_enabled")) return null;
140561
- return this.workspaceConversationKey();
140601
+ const base = this.workspaceConversationKey();
140602
+ if (!base) return null;
140603
+ return branchConversationIdentity(base, this.currentBranchNodeId());
140604
+ }
140605
+ /**
140606
+ * The hot folded-history cache belongs to one branch node. Legacy entries
140607
+ * (created before branch scoping) belong to the root node, which is where
140608
+ * they were produced.
140609
+ */
140610
+ compressionCacheForBranch(entries) {
140611
+ const branch = String(this.currentBranchNodeId() || "");
140612
+ const root3 = String(this.conversationTree()?.rootNodeId || "");
140613
+ return (Array.isArray(entries) ? entries : []).flatMap((entry) => {
140614
+ const owner = String(entry.branchNodeId || root3 || "");
140615
+ if (branch && owner && owner !== branch) return [];
140616
+ if (branch && !owner) return [];
140617
+ return [{ ...entry, branchNodeId: owner || branch || void 0, messages: [...entry.messages] }];
140618
+ });
140619
+ }
140620
+ /**
140621
+ * Persist the union of every branch's folded history: the live in-memory
140622
+ * cache only holds the active branch, and the stored entry holds the rest.
140623
+ */
140624
+ mergedCompressionCacheForPersist(storedEntry) {
140625
+ const branch = String(this.currentBranchNodeId() || "");
140626
+ const others = (storedEntry?.compressionCache || []).filter((entry) => {
140627
+ const owner = String(entry.branchNodeId || "");
140628
+ return !!owner && owner !== branch;
140629
+ });
140630
+ return [...others, ...this.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] }))];
140562
140631
  }
140563
140632
  coldCompressionEntries() {
140564
140633
  const scopeKey = this.compressionArchiveScopeKey();
@@ -140621,7 +140690,8 @@ ${text3.slice(-tailChars).trimStart()}`;
140621
140690
  foldedEntries: messages.length,
140622
140691
  foldedChars,
140623
140692
  model,
140624
- fallback
140693
+ fallback,
140694
+ branchNodeId: this.currentBranchNodeId() || void 0
140625
140695
  });
140626
140696
  this.nextCompressionCacheId += 1;
140627
140697
  const maxEntries = Math.max(0, Math.floor(this.config.getNum("context", "compression_cache_max") || 8));
@@ -141702,6 +141772,81 @@ var GuardedContinuationStore = class {
141702
141772
  return { ...build2, input: cloneInput(build2.input) };
141703
141773
  });
141704
141774
  }
141775
+ /**
141776
+ * dev-0.6.4 hotfix: 当某个被受理的 Build 失败/取消后,它的后继会永远无法被
141777
+ * claim(`claimBuild` 只认「父 = 已提交 frontier」),整条队列就此卡死,新提交
141778
+ * 也只是接在失败链后面。
141779
+ *
141780
+ * 这里提供一条**显式**修复通道:把被阻断的排队 Build 重新挂到当前已提交的
141781
+ * frontier 上,保持原有队列顺序,并为每一次改挂写入 `BuildQueueRepaired`
141782
+ * 审计事件(记录旧 parent / 新 parent / 原因)。它不是静默 rebase:调用方是
141783
+ * 用户的显式动作(恢复队列)或一次新的入队命令。
141784
+ */
141785
+ repairBlockedQueue(input) {
141786
+ return this.mutate((ledger) => {
141787
+ const branch = this.requireBranch(ledger, input);
141788
+ const frontier = branch.headBuildId || null;
141789
+ const queued = Object.values(ledger.builds).filter((build2) => build2.branchId === branch.branchId && build2.status === "QUEUED").sort((a, b) => a.queueSequence - b.queueSequence);
141790
+ const repaired = [];
141791
+ let chainParent = frontier;
141792
+ let mutated = false;
141793
+ for (const build2 of queued) {
141794
+ if ((build2.parentBuildId || null) !== (chainParent || null)) {
141795
+ const previousParentBuildId = build2.parentBuildId;
141796
+ build2.parentBuildId = chainParent;
141797
+ build2.waitingReason = void 0;
141798
+ build2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
141799
+ repaired.push(build2.buildId);
141800
+ mutated = true;
141801
+ this.appendEvent(ledger, {
141802
+ type: "BuildQueueRepaired",
141803
+ workspaceId: this.workspaceId,
141804
+ rootId: branch.rootId,
141805
+ branchId: branch.branchId,
141806
+ buildId: build2.buildId,
141807
+ attemptId: null,
141808
+ fence: null,
141809
+ payload: {
141810
+ previousParentBuildId,
141811
+ newParentBuildId: chainParent,
141812
+ reason: String(input.reason || "committed-frontier-repair")
141813
+ }
141814
+ });
141815
+ }
141816
+ chainParent = build2.buildId;
141817
+ }
141818
+ if (mutated) {
141819
+ branch.queueRevision += 1;
141820
+ branch.lifecycleRevision += 1;
141821
+ branch.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
141822
+ }
141823
+ return { repaired, frontier };
141824
+ });
141825
+ }
141826
+ /**
141827
+ * dev-0.6.4 hotfix: 读取视角的受阻推导。
141828
+ *
141829
+ * 一个 QUEUED Build 只有在它的父正好是「已提交 frontier」或队列里位于它之前
141830
+ * 的可执行 Build 时才可能被 claim(见 `claimBuild`)。这里按队列顺序推导哪些行
141831
+ * 目前不可执行:父不是链上前驱、或父已 FAILED/CANCELLED 的行即为受阻,且受阻点
141832
+ * 之后的所有行同样受阻。
141833
+ */
141834
+ blockedQueuedBuildIds(ledger, branch) {
141835
+ const blocked = /* @__PURE__ */ new Set();
141836
+ const queued = Object.values(ledger.builds).filter((build2) => build2.branchId === branch.branchId && build2.status === "QUEUED").sort((a, b) => a.queueSequence - b.queueSequence);
141837
+ let chainParent = branch.headBuildId || null;
141838
+ for (const build2 of queued) {
141839
+ const parent2 = build2.parentBuildId ? ledger.builds[build2.parentBuildId] : null;
141840
+ const parentIsChainPredecessor = (build2.parentBuildId || null) === (chainParent || null);
141841
+ const parentFailed = !!parent2 && (parent2.status === "FAILED" || parent2.status === "CANCELLED");
141842
+ if (!parentIsChainPredecessor || parentFailed) {
141843
+ blocked.add(build2.buildId);
141844
+ continue;
141845
+ }
141846
+ chainParent = build2.buildId;
141847
+ }
141848
+ return blocked;
141849
+ }
141705
141850
  deleteQueuedBuild(input) {
141706
141851
  this.mutate((ledger) => {
141707
141852
  const build2 = ledger.builds[input.buildId];
@@ -142103,7 +142248,12 @@ var GuardedContinuationStore = class {
142103
142248
  }
142104
142249
  snapshotUnlocked(ledger, branch) {
142105
142250
  const guard = ledger.guards[branch.branchId] || defaultGuard(branch.branchId);
142106
- const builds = Object.values(ledger.builds).filter((build2) => build2.branchId === branch.branchId).sort((a, b) => a.queueSequence - b.queueSequence).map((build2) => ({ ...build2, input: cloneInput(build2.input) }));
142251
+ const blockedBuildIds = this.blockedQueuedBuildIds(ledger, branch);
142252
+ const builds = Object.values(ledger.builds).filter((build2) => build2.branchId === branch.branchId).sort((a, b) => a.queueSequence - b.queueSequence).map((build2) => ({
142253
+ ...build2,
142254
+ input: cloneInput(build2.input),
142255
+ waitingReason: build2.waitingReason || (blockedBuildIds.has(build2.buildId) ? "DEPENDENCY_FAILED" : void 0)
142256
+ }));
142107
142257
  return {
142108
142258
  workspaceId: this.workspaceId,
142109
142259
  rootId: branch.rootId,
@@ -142706,11 +142856,25 @@ var ConversationKernel = class {
142706
142856
  queueItems(target) {
142707
142857
  const runtime = this.findRuntime(target);
142708
142858
  if (!runtime) return [];
142859
+ let waitingByBuildId = /* @__PURE__ */ new Map();
142860
+ const continuation = this.continuationStore(runtime);
142861
+ if (continuation) {
142862
+ try {
142863
+ const snapshot2 = continuation.store.snapshot({
142864
+ workspaceId: continuation.workspaceId,
142865
+ rootId: continuation.rootId,
142866
+ branchId: continuation.branchId
142867
+ });
142868
+ waitingByBuildId = new Map(snapshot2.builds.filter((build2) => !!build2.waitingReason).map((build2) => [build2.buildId, String(build2.waitingReason)]));
142869
+ } catch {
142870
+ }
142871
+ }
142709
142872
  return runtime.pendingNextTurn.flatMap((item) => {
142710
142873
  if (item.queueMode !== "followUp") return [];
142711
142874
  if (typeof item.message === "string") return [];
142712
142875
  const id = String(item.message.clientMessageId || "");
142713
142876
  if (!id) return [];
142877
+ const buildId = String(item.message.buildId || "");
142714
142878
  return [{
142715
142879
  id,
142716
142880
  buildId: item.message.buildId,
@@ -142723,7 +142887,9 @@ var ConversationKernel = class {
142723
142887
  images: item.message.images?.map((image) => ({ ...image })),
142724
142888
  branchNodeId: item.message.branchNodeId,
142725
142889
  branchPath: item.message.branchPath?.slice(),
142726
- modelSelection: item.message.modelSelection
142890
+ modelSelection: item.message.modelSelection,
142891
+ waitingReason: waitingByBuildId.get(buildId),
142892
+ blocked: waitingByBuildId.has(buildId)
142727
142893
  }];
142728
142894
  });
142729
142895
  }
@@ -142997,6 +143163,35 @@ ${text3}`;
142997
143163
  }
142998
143164
  return runtime.queuePaused;
142999
143165
  }
143166
+ /**
143167
+ * dev-0.6.4 hotfix: 显式修复被失败前置阻断的排队行。
143168
+ *
143169
+ * 0.6.3 的合约是「失败/取消阻断后继,且不静默跳过」,所以这里不挂在恢复动作上
143170
+ * 自动执行;它必须由用户的显式动作触发(界面上的“修复受阻队列”按钮)。每一次
143171
+ * 改挂都写入 `BuildQueueRepaired` 事件并发布可见状态,触发者与原因可审计。
143172
+ */
143173
+ repairBlockedQueueNow(runtime) {
143174
+ const continuation = this.continuationStore(runtime);
143175
+ if (!continuation) return 0;
143176
+ try {
143177
+ const result = continuation.store.repairBlockedQueue({
143178
+ workspaceId: continuation.workspaceId,
143179
+ rootId: continuation.rootId,
143180
+ branchId: continuation.branchId,
143181
+ reason: "explicit-user-repair"
143182
+ });
143183
+ if (result.repaired.length) {
143184
+ runtime.runner.recordWorkStatus(
143185
+ `${result.repaired.length} queued row(s) blocked by a failed predecessor were re-anchored to the last committed build; the queue resumes in order.`
143186
+ );
143187
+ this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
143188
+ }
143189
+ return result.repaired.length;
143190
+ } catch (error2) {
143191
+ runtime.runner.recordWorkStatus(`Queue repair failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
143192
+ return 0;
143193
+ }
143194
+ }
143000
143195
  queueAction(target, action, input = {}) {
143001
143196
  const normalized = this.normalizeTarget(target);
143002
143197
  const runtime = this.findRuntime(normalized) || this.runtime(normalized, {
@@ -143031,6 +143226,8 @@ ${text3}`;
143031
143226
  this.setQueuePaused(runtime.target, !runtime.queuePaused);
143032
143227
  } else if (action === "set_pause") {
143033
143228
  this.setQueuePaused(runtime.target, input.paused === true);
143229
+ } else if (action === "repair_blocked") {
143230
+ this.repairBlockedQueueNow(runtime);
143034
143231
  } else if (action === "guide") {
143035
143232
  if (input.text !== void 0) this.updateQueueItem(runtime.target, String(input.id || ""), input.text, input);
143036
143233
  const item = this.queueItems(runtime.target).find((entry) => entry.id === String(input.id || ""));
@@ -85,6 +85,8 @@ export interface CompressionCacheEntry {
85
85
  foldedChars: number;
86
86
  model: string;
87
87
  fallback: boolean;
88
+ /** Branch node that owns this folded history. Sibling pages must never read it. */
89
+ branchNodeId?: string;
88
90
  }
89
91
  type ConversationModelSelection = {
90
92
  kind: 'auto';
@@ -528,6 +530,16 @@ export declare class Agent {
528
530
  activeModelName(): string;
529
531
  modelSelectionValue(): string;
530
532
  currentWorkRunId(): string;
533
+ /**
534
+ * The branch node that owns the currently active Build. Provider session ids
535
+ * and branch-scoped caches must use this identity instead of "the branch that
536
+ * happens to be active", otherwise a concurrently running sibling branch
537
+ * (branch-communication mode) or a page switched mid-Build would read another
538
+ * branch's remote session and folded history.
539
+ */
540
+ activeWorkRunBranchId(): string;
541
+ /** Full conversation identity of one branch node (shared by every branch kind). */
542
+ branchConversationIdentity(branchNodeId: string): string;
531
543
  activeModelConfig(): ReturnType<ConfigManager['allModels']>[number] | undefined;
532
544
  requestedModelSelection(): ModelSelection;
533
545
  resetAutoRoute(): void;
@@ -1229,6 +1241,17 @@ export declare class Agent {
1229
1241
  private formatCompressionSummary;
1230
1242
  private persistCompressedHistory;
1231
1243
  private compressionArchiveScopeKey;
1244
+ /**
1245
+ * The hot folded-history cache belongs to one branch node. Legacy entries
1246
+ * (created before branch scoping) belong to the root node, which is where
1247
+ * they were produced.
1248
+ */
1249
+ private compressionCacheForBranch;
1250
+ /**
1251
+ * Persist the union of every branch's folded history: the live in-memory
1252
+ * cache only holds the active branch, and the stored entry holds the rest.
1253
+ */
1254
+ private mergedCompressionCacheForPersist;
1232
1255
  private coldCompressionEntries;
1233
1256
  private compressionArchiveEntryCount;
1234
1257
  private archiveColdCompressionEntries;
@@ -58,6 +58,7 @@ const skills_1 = require("./skills");
58
58
  const flow_1 = require("./flow");
59
59
  const memoryLab_1 = require("./memoryLab");
60
60
  const agentKernelRunner_1 = require("./agentKernelRunner");
61
+ const branchIdentity_1 = require("./branchIdentity");
61
62
  const toolPolicy_1 = require("./toolPolicy");
62
63
  const conversationTarget_1 = require("./conversationTarget");
63
64
  const utilityHostToolBridge_1 = require("./utilityHostToolBridge");
@@ -1020,6 +1021,22 @@ class Agent {
1020
1021
  currentWorkRunId() {
1021
1022
  return this.activeWorkRunId || this.finalizingWorkRunId || '';
1022
1023
  }
1024
+ /**
1025
+ * The branch node that owns the currently active Build. Provider session ids
1026
+ * and branch-scoped caches must use this identity instead of "the branch that
1027
+ * happens to be active", otherwise a concurrently running sibling branch
1028
+ * (branch-communication mode) or a page switched mid-Build would read another
1029
+ * branch's remote session and folded history.
1030
+ */
1031
+ activeWorkRunBranchId() {
1032
+ const runId = this.currentWorkRunId();
1033
+ const run = runId ? this.workRuns.find(item => String(item.runId || '') === runId) : undefined;
1034
+ return String(run?.branchNodeId || '') || this.currentBranchNodeId();
1035
+ }
1036
+ /** Full conversation identity of one branch node (shared by every branch kind). */
1037
+ branchConversationIdentity(branchNodeId) {
1038
+ return (0, branchIdentity_1.branchConversationIdentity)(this.activeConversationId || 'default', branchNodeId);
1039
+ }
1023
1040
  activeModelConfig() {
1024
1041
  const deployment = this.activeDeployment();
1025
1042
  return deployment ? this.config.findDeployment(deployment) : this.config.findModel(this.model);
@@ -3615,6 +3632,10 @@ class Agent {
3615
3632
  && ((targetGuideNodeId && group.pageGuideIds?.[parentNodeId] === targetGuideNodeId)
3616
3633
  || (!targetGuideNodeId && targetMessageId && group.pageMessageIds?.[parentNodeId] === targetMessageId))));
3617
3634
  const branchId = crypto.randomUUID();
3635
+ // dev-0.6.4: 新分支(用户分页编辑与实验性分支交流共用这一事务)必须拿到
3636
+ // 全新的全对话 id。复用节点 id 会让两个分支共享 provider 会话序列、
3637
+ // 压缩历史与本地缓存,正是「跨分支缓存」必须禁止的情形。
3638
+ (0, branchIdentity_1.assertFreshBranchIdentity)(clean || this.activeConversationId || 'default', branchId, Object.keys(tree.nodes));
3618
3639
  const branch = this.treeNodeFromEntry(branchId, parentNodeId, index, text, {
3619
3640
  ...entry,
3620
3641
  chatMessages: sourceNode.chatMessages.slice(0, index),
@@ -4317,7 +4338,7 @@ class Agent {
4317
4338
  chatMessages: [...this.chatMessages],
4318
4339
  history: [...this.history],
4319
4340
  providerUsage: this.conversationProviderUsage(),
4320
- compressionCache: [...this.compressionCache],
4341
+ compressionCache: this.mergedCompressionCacheForPersist(stored.conversations[stateKey]),
4321
4342
  branchMailbox: [...this.branchMailbox],
4322
4343
  branchCommunication: this.branchCommunicationEnabled,
4323
4344
  plan: this.normalizeConversationPlan(this.conversationPlan),
@@ -4377,7 +4398,7 @@ class Agent {
4377
4398
  if (saved) {
4378
4399
  this.restoreProviderUsage(saved.providerUsage);
4379
4400
  this.history = [...saved.history];
4380
- this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
4401
+ this.compressionCache = this.compressionCacheForBranch(saved.compressionCache);
4381
4402
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
4382
4403
  this.branchMailbox = (saved.branchMailbox || []).map(message => ({ ...message }));
4383
4404
  this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map(message => Number(message.sequence) || 0)) + 1;
@@ -4406,7 +4427,7 @@ class Agent {
4406
4427
  this.restoreProviderUsage(persisted?.providerUsage);
4407
4428
  const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
4408
4429
  this.history = repairedHistory.messages;
4409
- this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
4430
+ this.compressionCache = this.compressionCacheForBranch(persisted?.compressionCache);
4410
4431
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
4411
4432
  this.branchMailbox = (persisted?.branchMailbox || []).map(message => ({ ...message }));
4412
4433
  this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map(message => Number(message.sequence) || 0)) + 1;
@@ -9795,7 +9816,41 @@ class Agent {
9795
9816
  compressionArchiveScopeKey() {
9796
9817
  if (this.isSubagentRuntime || !this.config.getBool('context', 'compression_archive_enabled'))
9797
9818
  return null;
9798
- return this.workspaceConversationKey();
9819
+ const base = this.workspaceConversationKey();
9820
+ if (!base)
9821
+ return null;
9822
+ // dev-0.6.4: 折叠历史必须按分支节点隔离——用户分页产生的分支与实验性分支
9823
+ // 交流产生的分支共用这一条身份规则,兄弟分支不得互相读取压缩归档。
9824
+ return (0, branchIdentity_1.branchConversationIdentity)(base, this.currentBranchNodeId());
9825
+ }
9826
+ /**
9827
+ * The hot folded-history cache belongs to one branch node. Legacy entries
9828
+ * (created before branch scoping) belong to the root node, which is where
9829
+ * they were produced.
9830
+ */
9831
+ compressionCacheForBranch(entries) {
9832
+ const branch = String(this.currentBranchNodeId() || '');
9833
+ const root = String(this.conversationTree()?.rootNodeId || '');
9834
+ return (Array.isArray(entries) ? entries : []).flatMap(entry => {
9835
+ const owner = String(entry.branchNodeId || root || '');
9836
+ if (branch && owner && owner !== branch)
9837
+ return [];
9838
+ if (branch && !owner)
9839
+ return [];
9840
+ return [{ ...entry, branchNodeId: owner || branch || undefined, messages: [...entry.messages] }];
9841
+ });
9842
+ }
9843
+ /**
9844
+ * Persist the union of every branch's folded history: the live in-memory
9845
+ * cache only holds the active branch, and the stored entry holds the rest.
9846
+ */
9847
+ mergedCompressionCacheForPersist(storedEntry) {
9848
+ const branch = String(this.currentBranchNodeId() || '');
9849
+ const others = (storedEntry?.compressionCache || []).filter(entry => {
9850
+ const owner = String(entry.branchNodeId || '');
9851
+ return !!owner && owner !== branch;
9852
+ });
9853
+ return [...others, ...this.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] }))];
9799
9854
  }
9800
9855
  coldCompressionEntries() {
9801
9856
  const scopeKey = this.compressionArchiveScopeKey();
@@ -9872,6 +9927,7 @@ class Agent {
9872
9927
  foldedChars,
9873
9928
  model,
9874
9929
  fallback,
9930
+ branchNodeId: this.currentBranchNodeId() || undefined,
9875
9931
  });
9876
9932
  this.nextCompressionCacheId += 1;
9877
9933
  const maxEntries = Math.max(0, Math.floor(this.config.getNum('context', 'compression_cache_max') || 8));
@@ -39,6 +39,7 @@ exports.resetPublicAssistantDeltaFilter = resetPublicAssistantDeltaFilter;
39
39
  exports.runAgentKernel = runAgentKernel;
40
40
  exports.routeToolSurfaceV2 = routeToolSurfaceV2;
41
41
  exports.toolResultObjectiveOutcome = toolResultObjectiveOutcome;
42
+ const branchIdentity_1 = require("./branchIdentity");
42
43
  const fs = __importStar(require("fs"));
43
44
  const path = __importStar(require("path"));
44
45
  const terminalTakeover_1 = require("../tools/terminalTakeover");
@@ -75,13 +76,25 @@ const HIDDEN_LINE_PREFIXES = [
75
76
  * a fork that reuses the root conversation id can read the other branch's
76
77
  * history. Keep the id stable within one branch (normal sequential turns still
77
78
  * cache-hit) and change it the moment the runtime branch changes.
79
+ *
80
+ * dev-0.6.4: 分支身份固定在 Build 启动时创建的那条分支节点上(用户分页分支与
81
+ * 实验性分支交流分支共用同一规则)。这里绝不读取「当前正在查看/激活的分支」,
82
+ * 否则正在并行运行的兄弟分支或中途切换的页面会把请求绑到别的分支的远程会话。
78
83
  */
79
84
  function providerSessionIdentity(agent) {
80
85
  const conversationId = String(agent.activeConversationId || 'default');
86
+ let runBranchId = '';
87
+ try {
88
+ runBranchId = String(agent.activeWorkRunBranchId() || '');
89
+ }
90
+ catch {
91
+ runBranchId = '';
92
+ }
93
+ if (runBranchId)
94
+ return (0, branchIdentity_1.branchConversationIdentity)(conversationId, runBranchId);
81
95
  try {
82
96
  const snapshot = agent.getConversationSnapshot(conversationId);
83
- const branchId = String(snapshot.runtimeBranchId || snapshot.activeBranchId || '');
84
- return branchId ? `${conversationId}::branch:${branchId}` : conversationId;
97
+ return (0, branchIdentity_1.branchConversationIdentity)(conversationId, String(snapshot.runtimeBranchId || snapshot.activeBranchId || ''));
85
98
  }
86
99
  catch {
87
100
  return conversationId;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Branch conversation identity (dev-0.6.4).
3
+ *
4
+ * A conversation owns a tree of branch nodes. Every node is its own full
5
+ * conversation: the provider-side session sequence id, the folded-history
6
+ * archive and every local transcript cache are scoped by the node id, so
7
+ * sibling pages can never read each other's remote session or cached history.
8
+ *
9
+ * The user's pagination edits and the experimental branch-communication
10
+ * branches are created by the same `Agent.branchConversation` transaction, so
11
+ * both kinds of branch must share this single identity function. Never derive
12
+ * the identity from "the branch that happens to be active" at request time.
13
+ */
14
+ export declare const BRANCH_IDENTITY_SEPARATOR = "::branch:";
15
+ /** The full conversation id of one branch node: `<conversationId>::branch:<branchNodeId>`. */
16
+ export declare function branchConversationIdentity(conversationId: string, branchNodeId: string): string;
17
+ /** True when a value already carries a branch node identity. */
18
+ export declare function isBranchConversationIdentity(value: string): boolean;
19
+ /**
20
+ * Workspace-wide cache scope for one branch. Used by caches that must never be
21
+ * shared across sibling branches (compression history, provider payloads).
22
+ */
23
+ export declare function branchCacheScopeKey(workspaceId: string, conversationId: string, branchNodeId: string): string;
24
+ /**
25
+ * A branch may never reuse the identity of another node. Callers that mint a
26
+ * new page assert this before the node becomes reachable.
27
+ */
28
+ export declare function assertFreshBranchIdentity(conversationId: string, branchNodeId: string, existing: Iterable<string>): void;
29
+ //# sourceMappingURL=branchIdentity.d.ts.map
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * Branch conversation identity (dev-0.6.4).
4
+ *
5
+ * A conversation owns a tree of branch nodes. Every node is its own full
6
+ * conversation: the provider-side session sequence id, the folded-history
7
+ * archive and every local transcript cache are scoped by the node id, so
8
+ * sibling pages can never read each other's remote session or cached history.
9
+ *
10
+ * The user's pagination edits and the experimental branch-communication
11
+ * branches are created by the same `Agent.branchConversation` transaction, so
12
+ * both kinds of branch must share this single identity function. Never derive
13
+ * the identity from "the branch that happens to be active" at request time.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.BRANCH_IDENTITY_SEPARATOR = void 0;
17
+ exports.branchConversationIdentity = branchConversationIdentity;
18
+ exports.isBranchConversationIdentity = isBranchConversationIdentity;
19
+ exports.branchCacheScopeKey = branchCacheScopeKey;
20
+ exports.assertFreshBranchIdentity = assertFreshBranchIdentity;
21
+ exports.BRANCH_IDENTITY_SEPARATOR = '::branch:';
22
+ /** The full conversation id of one branch node: `<conversationId>::branch:<branchNodeId>`. */
23
+ function branchConversationIdentity(conversationId, branchNodeId) {
24
+ const conversation = String(conversationId || '').trim() || 'default';
25
+ const branch = String(branchNodeId || '').trim();
26
+ return branch ? `${conversation}${exports.BRANCH_IDENTITY_SEPARATOR}${branch}` : conversation;
27
+ }
28
+ /** True when a value already carries a branch node identity. */
29
+ function isBranchConversationIdentity(value) {
30
+ return String(value || '').includes(exports.BRANCH_IDENTITY_SEPARATOR);
31
+ }
32
+ /**
33
+ * Workspace-wide cache scope for one branch. Used by caches that must never be
34
+ * shared across sibling branches (compression history, provider payloads).
35
+ */
36
+ function branchCacheScopeKey(workspaceId, conversationId, branchNodeId) {
37
+ const workspace = String(workspaceId || '').trim() || 'none';
38
+ return `${workspace}::${branchConversationIdentity(conversationId, branchNodeId)}`;
39
+ }
40
+ /**
41
+ * A branch may never reuse the identity of another node. Callers that mint a
42
+ * new page assert this before the node becomes reachable.
43
+ */
44
+ function assertFreshBranchIdentity(conversationId, branchNodeId, existing) {
45
+ const branch = String(branchNodeId || '').trim();
46
+ if (!branch)
47
+ throw new Error('Branch conversation identity requires a branch node id.');
48
+ for (const known of existing) {
49
+ if (String(known || '') === branch) {
50
+ throw new Error(`Branch conversation identity ${branchConversationIdentity(conversationId, branch)} is already in use.`);
51
+ }
52
+ }
53
+ }
54
+ //# sourceMappingURL=branchIdentity.js.map
@@ -146,7 +146,7 @@ export interface FinalRecord {
146
146
  export interface ContinuationEvent {
147
147
  eventId: string;
148
148
  cursor: number;
149
- type: 'BuildAccepted' | 'AttemptStarted' | 'AttemptHeartbeat' | 'BuildCommitted' | 'BuildFailed' | 'BuildCancelled' | 'BranchForked' | 'BranchPaused' | 'BranchResumed' | 'StaleWriteRejected';
149
+ type: 'BuildAccepted' | 'AttemptStarted' | 'AttemptHeartbeat' | 'BuildCommitted' | 'BuildFailed' | 'BuildCancelled' | 'BranchForked' | 'BranchPaused' | 'BranchResumed' | 'BuildQueueRepaired' | 'StaleWriteRejected';
150
150
  workspaceId: WorkspaceId;
151
151
  rootId: RootId;
152
152
  branchId: BranchId;
@@ -47,6 +47,34 @@ export declare class GuardedContinuationStore {
47
47
  buildId: BuildId;
48
48
  input: BuildInput;
49
49
  }): BuildRecord;
50
+ /**
51
+ * dev-0.6.4 hotfix: 当某个被受理的 Build 失败/取消后,它的后继会永远无法被
52
+ * claim(`claimBuild` 只认「父 = 已提交 frontier」),整条队列就此卡死,新提交
53
+ * 也只是接在失败链后面。
54
+ *
55
+ * 这里提供一条**显式**修复通道:把被阻断的排队 Build 重新挂到当前已提交的
56
+ * frontier 上,保持原有队列顺序,并为每一次改挂写入 `BuildQueueRepaired`
57
+ * 审计事件(记录旧 parent / 新 parent / 原因)。它不是静默 rebase:调用方是
58
+ * 用户的显式动作(恢复队列)或一次新的入队命令。
59
+ */
60
+ repairBlockedQueue(input: {
61
+ workspaceId: string;
62
+ rootId: RootId;
63
+ branchId: BranchId;
64
+ reason?: string;
65
+ }): {
66
+ repaired: BuildId[];
67
+ frontier: BuildId | null;
68
+ };
69
+ /**
70
+ * dev-0.6.4 hotfix: 读取视角的受阻推导。
71
+ *
72
+ * 一个 QUEUED Build 只有在它的父正好是「已提交 frontier」或队列里位于它之前
73
+ * 的可执行 Build 时才可能被 claim(见 `claimBuild`)。这里按队列顺序推导哪些行
74
+ * 目前不可执行:父不是链上前驱、或父已 FAILED/CANCELLED 的行即为受阻,且受阻点
75
+ * 之后的所有行同样受阻。
76
+ */
77
+ private blockedQueuedBuildIds;
50
78
  deleteQueuedBuild(input: {
51
79
  workspaceId: string;
52
80
  rootId: RootId;