newmark-agent 0.6.2 → 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.
@@ -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';
@@ -294,8 +296,15 @@ export interface ConversationBranchGroupSnapshot {
294
296
  export interface ConversationContinuation {
295
297
  content: string;
296
298
  queueMode: 'steer' | 'followUp';
299
+ buildId?: string;
297
300
  clientMessageId?: string;
298
301
  runId?: string;
302
+ branchNodeId?: string;
303
+ branchPath?: string[];
304
+ ledgerBranchId?: string;
305
+ targetRuntimeKey?: string;
306
+ workspaceKey?: string;
307
+ modelSelection?: string;
299
308
  images?: Array<{
300
309
  dataUrl: string;
301
310
  name?: string;
@@ -521,6 +530,16 @@ export declare class Agent {
521
530
  activeModelName(): string;
522
531
  modelSelectionValue(): string;
523
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;
524
543
  activeModelConfig(): ReturnType<ConfigManager['allModels']>[number] | undefined;
525
544
  requestedModelSelection(): ModelSelection;
526
545
  resetAutoRoute(): void;
@@ -787,6 +806,13 @@ export declare class Agent {
787
806
  */
788
807
  private deriveConversationTitleFromProvider;
789
808
  private conversationTitleFailureCause;
809
+ /**
810
+ * Deterministic provider rejections (bad key, forbidden endpoint, unknown
811
+ * model, invalid request, payment required) cannot succeed by retrying the
812
+ * identical title probe. Only transient transport, timeout, rate-limit and
813
+ * provider-internal failures enter the bounded 5-stage backoff.
814
+ */
815
+ private conversationTitleFailureIsRetryable;
790
816
  private waitForConversationTitleRetry;
791
817
  /** Legacy completion hook retained as a no-op; naming is pre-response only. */
792
818
  private maybeAutoRenameConversationFromRun;
@@ -1215,6 +1241,17 @@ export declare class Agent {
1215
1241
  private formatCompressionSummary;
1216
1242
  private persistCompressedHistory;
1217
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;
1218
1255
  private coldCompressionEntries;
1219
1256
  private compressionArchiveEntryCount;
1220
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);
@@ -2735,8 +2752,15 @@ class Agent {
2735
2752
  deduped.set(key, {
2736
2753
  content,
2737
2754
  queueMode,
2755
+ buildId: String(raw.buildId || '').trim() || undefined,
2738
2756
  clientMessageId,
2739
2757
  runId: String(raw.runId || '').trim() || undefined,
2758
+ branchNodeId: String(raw.branchNodeId || '').trim() || undefined,
2759
+ branchPath: Array.isArray(raw.branchPath) ? raw.branchPath.map(id => String(id || '')).filter(Boolean) : undefined,
2760
+ ledgerBranchId: String(raw.ledgerBranchId || '').trim() || undefined,
2761
+ targetRuntimeKey: String(raw.targetRuntimeKey || '').trim() || undefined,
2762
+ workspaceKey: String(raw.workspaceKey || '').trim() || undefined,
2763
+ modelSelection: String(raw.modelSelection || '').trim() || undefined,
2740
2764
  images: images.length ? images : undefined,
2741
2765
  attachments: attachments.length ? attachments : undefined,
2742
2766
  hiddenUserInput: raw.hiddenUserInput === true,
@@ -3608,6 +3632,10 @@ class Agent {
3608
3632
  && ((targetGuideNodeId && group.pageGuideIds?.[parentNodeId] === targetGuideNodeId)
3609
3633
  || (!targetGuideNodeId && targetMessageId && group.pageMessageIds?.[parentNodeId] === targetMessageId))));
3610
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));
3611
3639
  const branch = this.treeNodeFromEntry(branchId, parentNodeId, index, text, {
3612
3640
  ...entry,
3613
3641
  chatMessages: sourceNode.chatMessages.slice(0, index),
@@ -3950,6 +3978,19 @@ class Agent {
3950
3978
  return 'provider returned invalid JSON';
3951
3979
  return 'provider request failed';
3952
3980
  }
3981
+ /**
3982
+ * Deterministic provider rejections (bad key, forbidden endpoint, unknown
3983
+ * model, invalid request, payment required) cannot succeed by retrying the
3984
+ * identical title probe. Only transient transport, timeout, rate-limit and
3985
+ * provider-internal failures enter the bounded 5-stage backoff.
3986
+ */
3987
+ conversationTitleFailureIsRetryable(error) {
3988
+ const message = error instanceof Error ? error.message : '';
3989
+ const status = message.match(/^\[LLM Error:\s*([1-5]\d{2})\]/)?.[1];
3990
+ if (!status)
3991
+ return true;
3992
+ return status === '408' || status === '429' || status.startsWith('5');
3993
+ }
3953
3994
  async waitForConversationTitleRetry(delayMs, signal) {
3954
3995
  if (signal?.aborted || delayMs <= 0)
3955
3996
  return;
@@ -3981,17 +4022,20 @@ class Agent {
3981
4022
  // Failure state belongs to this title attempt sequence, not the Agent or
3982
4023
  // another conversation. A later empty title replaces an earlier HTTP error.
3983
4024
  let lastFailure = 'provider returned an empty or unusable title';
4025
+ let terminalFailure = false;
3984
4026
  const requestTitle = async () => {
3985
4027
  lastFailure = 'provider returned an empty or unusable title';
4028
+ terminalFailure = false;
3986
4029
  try {
3987
4030
  return await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
3988
4031
  }
3989
4032
  catch (error) {
3990
4033
  lastFailure = this.conversationTitleFailureCause(error, provider);
4034
+ terminalFailure = !this.conversationTitleFailureIsRetryable(error);
3991
4035
  return '';
3992
4036
  }
3993
4037
  };
3994
- const exhaustedFailure = () => new Error(`Conversation title generation failed; ${lastFailure}. The first Agent request was not started. Retry the first input.`);
4038
+ const exhaustedFailure = () => new Error(`Conversation title generation failed for ${provider.name}/${modelName}; ${lastFailure}. The first Agent request was not started. Retry the first input or switch the model.`);
3995
4039
  if (!stateKey) {
3996
4040
  // Pure Agent/CLI mode has no workspace conversation file to rename, but
3997
4041
  // still uses the title request as the required first model-availability
@@ -3999,6 +4043,8 @@ class Agent {
3999
4043
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
4000
4044
  if (await requestTitle())
4001
4045
  return true;
4046
+ if (terminalFailure)
4047
+ break;
4002
4048
  if (signal?.aborted)
4003
4049
  return false;
4004
4050
  if (attempt < maxAttempts - 1)
@@ -4036,6 +4082,8 @@ class Agent {
4036
4082
  this.publishConversationTitle(title, conversationId);
4037
4083
  return true;
4038
4084
  }
4085
+ if (terminalFailure)
4086
+ break;
4039
4087
  if (signal?.aborted)
4040
4088
  return false;
4041
4089
  if (attempt < maxAttempts - 1)
@@ -4290,7 +4338,7 @@ class Agent {
4290
4338
  chatMessages: [...this.chatMessages],
4291
4339
  history: [...this.history],
4292
4340
  providerUsage: this.conversationProviderUsage(),
4293
- compressionCache: [...this.compressionCache],
4341
+ compressionCache: this.mergedCompressionCacheForPersist(stored.conversations[stateKey]),
4294
4342
  branchMailbox: [...this.branchMailbox],
4295
4343
  branchCommunication: this.branchCommunicationEnabled,
4296
4344
  plan: this.normalizeConversationPlan(this.conversationPlan),
@@ -4350,7 +4398,7 @@ class Agent {
4350
4398
  if (saved) {
4351
4399
  this.restoreProviderUsage(saved.providerUsage);
4352
4400
  this.history = [...saved.history];
4353
- this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
4401
+ this.compressionCache = this.compressionCacheForBranch(saved.compressionCache);
4354
4402
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
4355
4403
  this.branchMailbox = (saved.branchMailbox || []).map(message => ({ ...message }));
4356
4404
  this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map(message => Number(message.sequence) || 0)) + 1;
@@ -4379,7 +4427,7 @@ class Agent {
4379
4427
  this.restoreProviderUsage(persisted?.providerUsage);
4380
4428
  const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
4381
4429
  this.history = repairedHistory.messages;
4382
- this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
4430
+ this.compressionCache = this.compressionCacheForBranch(persisted?.compressionCache);
4383
4431
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
4384
4432
  this.branchMailbox = (persisted?.branchMailbox || []).map(message => ({ ...message }));
4385
4433
  this.nextBranchMessageSequence = Math.max(1, ...this.branchMailbox.map(message => Number(message.sequence) || 0)) + 1;
@@ -6015,7 +6063,7 @@ class Agent {
6015
6063
  let requestContext = null;
6016
6064
  const context = saved?.version === 1 ? saved.requestContext : null;
6017
6065
  if (context && typeof context.requestId === 'string' && typeof context.runId === 'string' && typeof context.at === 'string') {
6018
- requestContext = { requestId: context.requestId, runId: context.runId, model: String(context.model || ''), at: context.at,
6066
+ requestContext = { requestId: context.requestId, runId: context.runId, branchId: String(context.branchId || ''), contextHash: String(context.contextHash || ''), model: String(context.model || ''), at: context.at,
6019
6067
  estimatedTokens: 0, longHistoryTokens: 0, buildBlockTokens: 0, systemPromptTokens: 0, toolSchemaTokens: 0, messageCount: 0, hasImages: context.hasImages === true };
6020
6068
  for (const key of ['estimatedTokens', 'longHistoryTokens', 'buildBlockTokens', 'systemPromptTokens', 'toolSchemaTokens', 'messageCount', 'inputTokens', 'cacheReadTokens']) {
6021
6069
  const value = context[key];
@@ -6083,9 +6131,16 @@ class Agent {
6083
6131
  return request;
6084
6132
  }
6085
6133
  recordRequestContext(request, messages, system, tools, model) {
6134
+ const contextHash = crypto.createHash('sha256').update(JSON.stringify({
6135
+ model,
6136
+ system,
6137
+ tools,
6138
+ messages,
6139
+ })).digest('hex').slice(0, 24);
6086
6140
  const context = {
6087
6141
  ...(0, requestContextEstimate_1.estimateSubmittedContext)(messages, system, tools, this.compressionBuildBlockStart(messages)),
6088
- requestId: request.id, runId: this.currentWorkRunId(), model, at: new Date().toISOString(),
6142
+ requestId: request.id, runId: this.currentWorkRunId(), branchId: this.currentBranchNodeId(),
6143
+ contextHash, model, at: new Date().toISOString(),
6089
6144
  };
6090
6145
  this.mutateRequestUsage(request, usage => { usage.requestContext = context; });
6091
6146
  }
@@ -8187,6 +8242,13 @@ class Agent {
8187
8242
  if (!titleProvider || !titleModelName) {
8188
8243
  throw new Error('Conversation title generation failed; no resolved model deployment is available. No LLM configured. Add provider in Settings > Models.');
8189
8244
  }
8245
+ // The title probe is a hard gate before the first formal request. Show
8246
+ // the exact deployment being checked immediately so a slow or failing
8247
+ // first turn never looks like an unexplained freeze.
8248
+ this.emitWorkEvent({
8249
+ type: 'status',
8250
+ content: `Preparing the first response: checking ${titleProvider.name}/${titleModelName} and generating the conversation title.`,
8251
+ });
8190
8252
  const titled = await this.startFirstInputConversationTitle(firstResponseTitleInput.messageId, firstResponseTitleInput.input, titleProvider, titleModelName, titleIntelligence, processSignal);
8191
8253
  if (!titled)
8192
8254
  throw new Error('Conversation title generation failed; the first Agent request was not started. Retry the first input.');
@@ -9754,7 +9816,41 @@ class Agent {
9754
9816
  compressionArchiveScopeKey() {
9755
9817
  if (this.isSubagentRuntime || !this.config.getBool('context', 'compression_archive_enabled'))
9756
9818
  return null;
9757
- 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] }))];
9758
9854
  }
9759
9855
  coldCompressionEntries() {
9760
9856
  const scopeKey = this.compressionArchiveScopeKey();
@@ -9831,6 +9927,7 @@ class Agent {
9831
9927
  foldedChars,
9832
9928
  model,
9833
9929
  fallback,
9930
+ branchNodeId: this.currentBranchNodeId() || undefined,
9834
9931
  });
9835
9932
  this.nextCompressionCacheId += 1;
9836
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");
@@ -69,6 +70,36 @@ const HIDDEN_LINE_PREFIXES = [
69
70
  'analysis:',
70
71
  'analysis:',
71
72
  ];
73
+ /**
74
+ * Optional provider-side session cache identity. A remote session is mutable
75
+ * state, so it must never be shared by two branches of the same conversation:
76
+ * a fork that reuses the root conversation id can read the other branch's
77
+ * history. Keep the id stable within one branch (normal sequential turns still
78
+ * cache-hit) and change it the moment the runtime branch changes.
79
+ *
80
+ * dev-0.6.4: 分支身份固定在 Build 启动时创建的那条分支节点上(用户分页分支与
81
+ * 实验性分支交流分支共用同一规则)。这里绝不读取「当前正在查看/激活的分支」,
82
+ * 否则正在并行运行的兄弟分支或中途切换的页面会把请求绑到别的分支的远程会话。
83
+ */
84
+ function providerSessionIdentity(agent) {
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);
95
+ try {
96
+ const snapshot = agent.getConversationSnapshot(conversationId);
97
+ return (0, branchIdentity_1.branchConversationIdentity)(conversationId, String(snapshot.runtimeBranchId || snapshot.activeBranchId || ''));
98
+ }
99
+ catch {
100
+ return conversationId;
101
+ }
102
+ }
72
103
  function partialMarkerSuffix(value, marker) {
73
104
  const lower = value.toLowerCase();
74
105
  const target = marker.toLowerCase();
@@ -660,7 +691,11 @@ async function runAgentKernel(agent) {
660
691
  currentAgent.checkpointPeerInput();
661
692
  peerInputCheckpointed = true;
662
693
  }
663
- const { temperature, maxTokens, reasoningEffort } = currentProvider.intelligenceConfig(currentAgent.intelligence);
694
+ const { temperature, reasoningEffort } = currentProvider.intelligenceConfig(currentAgent.intelligence);
695
+ // A normal Agent reply has no client-side output cap. The provider
696
+ // endpoint owns its real limit; protocols that require a positive
697
+ // max_tokens resolve one internally.
698
+ const maxTokens = 0;
664
699
  const newmarkMessages = fromKernelMessages(context.messages).map(message => message.role === 'assistant'
665
700
  // Use the same public-content boundary on its first provider
666
701
  // submission and after persistence. Trimming only the durable
@@ -697,7 +732,7 @@ async function runAgentKernel(agent) {
697
732
  });
698
733
  }
699
734
  for await (const token of currentProvider.chatStreamWithTools(currentModelName, newmarkMessages, requestSystemPrompt, temperature, maxTokens, providerTools, options?.signal, reasoningEffort, currentAgent.config.getBool('context', 'provider_session_id')
700
- ? currentAgent.activeConversationId
735
+ ? providerSessionIdentity(currentAgent)
701
736
  : undefined)) {
702
737
  if (!firstTokenRecorded && ((token.type === 'text' && token.text) || (token.type === 'tool_call' && token.toolCall))) {
703
738
  firstTokenRecorded = true;
@@ -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
@@ -0,0 +1,197 @@
1
+ /**
2
+ * continuation-v1 contracts.
3
+ *
4
+ * These are the runtime-checked boundaries shared by the PC GUI, the hosted
5
+ * mobile API and the isolated runtimes. A view may read a snapshot, but it can
6
+ * never supply the execution identity that a write is allowed to use.
7
+ */
8
+ export type WorkspaceId = string;
9
+ export type RootId = string;
10
+ export type BranchId = string;
11
+ export type BuildId = string;
12
+ export type AttemptId = string;
13
+ export type CommandId = string;
14
+ export type SnapshotId = string;
15
+ export declare const CONTINUATION_SCHEMA_VERSION: 1;
16
+ export type BuildStatus = 'QUEUED' | 'RUNNING' | 'WAITING_TOOL' | 'WAITING_APPROVAL' | 'WAITING_SUBAGENT' | 'FINALIZING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED';
17
+ export type AttemptStatus = 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'EXPIRED';
18
+ export type ContinuationErrorCode = 'TARGET_REQUIRED' | 'ROUTE_MISMATCH' | 'QUEUE_REVISION_CONFLICT' | 'ANCHOR_NOT_IN_LINEAGE' | 'ANCHOR_NOT_COMMITTED' | 'IDEMPOTENCY_KEY_REUSE' | 'STALE_EXECUTION' | 'FINAL_ALREADY_COMMITTED' | 'BUILD_NOT_READY' | 'BRANCH_PAUSED' | 'EFFECT_UNCERTAIN' | 'UNKNOWN_BRANCH' | 'UNKNOWN_BUILD' | 'UNKNOWN_COMMAND';
19
+ export declare class ContinuationError extends Error {
20
+ readonly code: ContinuationErrorCode;
21
+ readonly detail?: Record<string, unknown> | undefined;
22
+ constructor(code: ContinuationErrorCode, message: string, detail?: Record<string, unknown> | undefined);
23
+ }
24
+ export interface CommandTarget {
25
+ workspaceId: WorkspaceId;
26
+ expectedRootId: RootId;
27
+ branchId: BranchId;
28
+ }
29
+ export interface BuildInput {
30
+ text: string;
31
+ images?: Array<{
32
+ dataUrl: string;
33
+ name?: string;
34
+ type?: string;
35
+ }>;
36
+ attachments?: Array<{
37
+ id: string;
38
+ name: string;
39
+ mimeType: string;
40
+ dataUrl?: string;
41
+ }>;
42
+ requestedMode?: string;
43
+ goalObjective?: string;
44
+ /** Provider-qualified model frozen at command acceptance time. */
45
+ modelSelection?: string;
46
+ /** Branch node captured when the user submitted the command. */
47
+ branchNodeId?: string;
48
+ branchPath?: string[];
49
+ }
50
+ export interface EnqueueBuildCommand {
51
+ schemaVersion: typeof CONTINUATION_SCHEMA_VERSION;
52
+ type: 'ENQUEUE_BUILD';
53
+ commandId: CommandId;
54
+ actorId: string;
55
+ target: CommandTarget;
56
+ expectedTailBuildId: BuildId | null;
57
+ expectedQueueRevision: number;
58
+ input: BuildInput;
59
+ recipeSnapshotId: SnapshotId;
60
+ createdAt: string;
61
+ }
62
+ export interface CommandReceipt {
63
+ schemaVersion: typeof CONTINUATION_SCHEMA_VERSION;
64
+ commandId: CommandId;
65
+ requestHash: string;
66
+ workspaceId: WorkspaceId;
67
+ rootId: RootId;
68
+ branchId: BranchId;
69
+ buildId: BuildId;
70
+ parentBuildId: BuildId | null;
71
+ queueSequence: number;
72
+ queueRevision: number;
73
+ lifecycleRevision: number;
74
+ acceptedAt: string;
75
+ duplicate?: boolean;
76
+ }
77
+ export interface ExecutionScope {
78
+ workspaceId: WorkspaceId;
79
+ rootId: RootId;
80
+ branchId: BranchId;
81
+ buildId: BuildId;
82
+ attemptId: AttemptId;
83
+ fence: number;
84
+ parentBuildId: BuildId | null;
85
+ baseContextSnapshotId: SnapshotId;
86
+ environmentSnapshotId: SnapshotId;
87
+ workerCapability: string;
88
+ }
89
+ export interface BranchGuardSnapshot {
90
+ branchId: BranchId;
91
+ activeBuildId: BuildId | null;
92
+ activeAttemptId: AttemptId | null;
93
+ fence: number;
94
+ leaseUntil: number;
95
+ workerCapability: string;
96
+ }
97
+ export interface QueueSnapshot {
98
+ workspaceId: WorkspaceId;
99
+ rootId: RootId;
100
+ branchId: BranchId;
101
+ headBuildId: BuildId | null;
102
+ tailBuildId: BuildId | null;
103
+ queueRevision: number;
104
+ lifecycleRevision: number;
105
+ paused: boolean;
106
+ activeBuildId: BuildId | null;
107
+ guard: BranchGuardSnapshot;
108
+ builds: BuildRecord[];
109
+ }
110
+ export interface BuildRecord {
111
+ buildId: BuildId;
112
+ workspaceId: WorkspaceId;
113
+ rootId: RootId;
114
+ branchId: BranchId;
115
+ parentBuildId: BuildId | null;
116
+ inputHash: string;
117
+ input: BuildInput;
118
+ recipeSnapshotId: SnapshotId;
119
+ status: BuildStatus;
120
+ queueSequence: number;
121
+ createdAt: string;
122
+ updatedAt: string;
123
+ finalId?: string;
124
+ waitingReason?: string;
125
+ }
126
+ export interface AttemptRecord {
127
+ attemptId: AttemptId;
128
+ buildId: BuildId;
129
+ branchId: BranchId;
130
+ fence: number;
131
+ status: AttemptStatus;
132
+ leaseUntil: number;
133
+ workerCapability: string;
134
+ startedAt: string;
135
+ endedAt?: string;
136
+ }
137
+ export interface FinalRecord {
138
+ finalId: string;
139
+ buildId: BuildId;
140
+ branchId: BranchId;
141
+ finalHash: string;
142
+ stateSnapshotId: SnapshotId;
143
+ text: string;
144
+ committedAt: string;
145
+ }
146
+ export interface ContinuationEvent {
147
+ eventId: string;
148
+ cursor: number;
149
+ type: 'BuildAccepted' | 'AttemptStarted' | 'AttemptHeartbeat' | 'BuildCommitted' | 'BuildFailed' | 'BuildCancelled' | 'BranchForked' | 'BranchPaused' | 'BranchResumed' | 'BuildQueueRepaired' | 'StaleWriteRejected';
150
+ workspaceId: WorkspaceId;
151
+ rootId: RootId;
152
+ branchId: BranchId;
153
+ buildId: BuildId | null;
154
+ attemptId: AttemptId | null;
155
+ fence: number | null;
156
+ at: string;
157
+ payload?: Record<string, unknown>;
158
+ }
159
+ export interface LedgerFile {
160
+ version: typeof CONTINUATION_SCHEMA_VERSION;
161
+ workspaceId: WorkspaceId;
162
+ roots: Record<RootId, {
163
+ rootId: RootId;
164
+ workspaceId: WorkspaceId;
165
+ defaultBranchId: BranchId;
166
+ createdAt: string;
167
+ }>;
168
+ branches: Record<BranchId, BranchRecord>;
169
+ builds: Record<BuildId, BuildRecord>;
170
+ attempts: Record<AttemptId, AttemptRecord>;
171
+ guards: Record<BranchId, BranchGuardSnapshot>;
172
+ commands: Record<string, CommandReceipt>;
173
+ finals: Record<BuildId, FinalRecord>;
174
+ events: ContinuationEvent[];
175
+ nextEventCursor: number;
176
+ nextQueueSequence: number;
177
+ }
178
+ export interface BranchRecord {
179
+ branchId: BranchId;
180
+ workspaceId: WorkspaceId;
181
+ rootId: RootId;
182
+ parentAnchorBuildId: BuildId | null;
183
+ headBuildId: BuildId | null;
184
+ tailBuildId: BuildId | null;
185
+ queueRevision: number;
186
+ lifecycleRevision: number;
187
+ paused: boolean;
188
+ createdAt: string;
189
+ updatedAt: string;
190
+ }
191
+ export declare function stableJson(value: unknown): string;
192
+ export declare function emptyLedger(workspaceId: WorkspaceId): LedgerFile;
193
+ export declare function defaultGuard(branchId: BranchId): BranchGuardSnapshot;
194
+ export declare function commandKey(actorId: string, commandId: CommandId): string;
195
+ export declare function buildInputHash(input: BuildInput): string;
196
+ export declare function requestHash(command: EnqueueBuildCommand): string;
197
+ //# sourceMappingURL=contracts.d.ts.map