@blade-hq/agent-client 2610.0.0-beta.70 → 2610.0.0-beta.72

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/README.md CHANGED
@@ -169,8 +169,16 @@ await client.sessions.deleteSession(id)
169
169
  await client.sessions.getSessionTurns(id) // 历史消息(投影格式,与实时流同构)
170
170
  const page = await client.sessions.getSessionTurnsPage(id) // 聊天首屏 recent page
171
171
  await client.sessions.getSessionTurnsPage(id, { before: page.nextBefore! })
172
+ // 从某条用户发言定位到最新(消息导航点窗口外的发言):调用参数是 SessionTurnsQuery
173
+ await client.sessions.getSessionTurnsPage(id, { fromEntryId: "entry-id" })
174
+ // 当前分支上全部可导航的用户发言目录(entry_id + 标题 + 时间,不含消息体)
175
+ const index = await client.sessions.getSessionTurnIndex(id) // SessionTurnIndex
176
+ const first = index.items[0] // SessionTurnIndexItem
172
177
  ```
173
178
 
179
+ `SessionTurnsQuery` 的 `before` 与 `fromEntryId` 互斥;定位命中不了当前分支时服务端返回
180
+ 404 / 409,而不是空窗口。目录的 `entryId` 与 `/turns` 的 `turn_id` 同源,可直接当 `before` 游标。
181
+
174
182
  会话插件可通过 `client.sessions.listSessionPlugins(id)` 查询,并用
175
183
  `client.sessions.setSessionPluginActivation(id, name, active)` 切换当前会话。列表项为 `SessionPlugin`;修改结果为 `SessionPluginActivation`,只返回选择和准备状态,安装事实需从列表获取。
176
184
 
@@ -736,12 +744,16 @@ HTML 一律判无效);`isInternalStatusToolUiCard` 识别内部「阶段进
736
744
  - **插件目录(REST,无会话)**:`PluginsResource`、`PluginCatalogEntry`(`client.plugins`)。首页这类还没有会话的界面用它读名称、业务名与图标 token
737
745
  - **账号电脑目录**:`AccountComputer`、`AccountComputerList`(`client.computers.listAccount()`,只读账号事实,不含会话授权)
738
746
  - **全站访问统计(REST)**:`SiteVisitsResource`、`SiteVisitsSummary`(`client.siteVisits.record()` 上报一次页面访问、`client.siteVisits.summary()` 只读快照)。这两个操作走独立匿名传输:不带 bearer、不触发 401 登录刷新,只依赖匿名访客 Cookie,供内置 Web 同源使用。
739
- - **会话资源(REST)**:`SessionsResource`、`CreateSessionRequest`、`ImportSessionOptions`、`AppCliDefinition`、`AppCliAttachment`、`AttachAppOptions`、`PaginatedSessionsResult`、`GlobalSearchResult`、`GlobalSearchResultItem`、`GlobalSearchConversationResult`、`GlobalSearchFileResult`、`SessionHistory`、`SessionContextStats`、`ResultFeedback`、`ResultFeedbackReason`、`ShareLinkResult`、`FileEntry`、`UploadFileEntry`、`UploadFilesOptions`、`SessionProfile`、`SessionDetail`、`SessionInfo`、`SessionStatus`、`SessionPortMapping`、`ModeId`、`TemplateId`、`PrimarySkillSnapshot`、`PrimarySkillParallelMode`
747
+ - **会话资源(REST)**:`SessionsResource`、`CreateSessionRequest`、`ImportSessionOptions`、`AppCliDefinition`、`AppCliAttachment`、`AttachAppOptions`、`PaginatedSessionsResult`、`GlobalSearchResult`、`GlobalSearchResultItem`、`GlobalSearchConversationResult`、`GlobalSearchFileResult`、`SessionHistory`、`SessionContextStats`、`ResultFeedback`、`ResultFeedbackReason`、`ShareLinkResult`、`FileEntry`、`UploadFileEntry`、`UploadFilesOptions`、`SessionProfile`、`SessionDetail`、`SessionInfo`、`SessionStatus`、`SessionPortMapping`、`ModeId`、`TemplateId`、`PrimarySkillSnapshot`、`PrimarySkillParallelMode`、`PrimarySkillStageSpec`、`PrimarySkillStepSpec`、`SkillEditorTemplateId`、`SessionSearchMatch`
748
+ - **会话历史取数**:`SessionTurnsQuery`、`SessionTurnsPage`、`SessionTurnIndex`、`SessionTurnIndexItem`、`isEntryGoneError`(`isEntryGoneError` 区分"这条发言已经不在当前分支上"与"这次没取到":前者说明 `fromEntryId` 定位目标失效,后者只说明本次请求失败)
740
749
  - **会话回放**:`ReplayState`、`ReplaySpeed`、`ReplayPreview`、`ReplaySnapshot`、`toReplaySnapshot`、`DEFAULT_REPLAY_SPEED`
741
750
  - **会话状态机**:`SessionHub`、`SessionConnectOptions`、`SessionState`、`SendOptions`、`ConnectionStatus`、`AskUserAnswerData`、`AgentLoopInfo`、`ActiveCompactionState`、`createInitialSessionState`、`AgentSessionEventName`
751
+ - **静态历史会话**:`AgentSession.fromTurns(sessionId, turns)` 构造的会话 `isStatic` 为 true——它没有目录接口,宿主拿已加载窗口推导航条目(`turnNavItemsFromMessages`);真实会话不要这么做,timeline 被重写后窗口里还是旧分支的发言,那些 entry id 已经点不到了
752
+ - **长会话跨页定位**:`AgentSession.locateEntry(entryId)`(按稳定 `entry_id` 取"从它往后"的窗口)、`AgentSession.cancelPendingLocate()`(作废在途的那次定位;它只中止定位自己的请求,不牵动运行结束 / 重连的权威刷新)。权威刷新(运行结束 / 重连补数 / 翻页之后的对账)不另记状态:一律按**当前窗口第一条发言**取窗口,那条已经不在分支上(404 / 409)时回落最新一页;`sessions.getSessionTurnIndex(sessionId)` 给出可导航的用户发言目录(`SessionTurnIndex` / `SessionTurnIndexItem`),`getSessionTurnsPage({ fromEntryId })` 与它共用同一份分支投影
742
753
  - **会话消息队列**:`SessionQueueSnapshot`、`QueuedMessage`、`QueuedMessageStatus`、`QueueAck`、`QueueOperationResult`、`EMPTY_SESSION_QUEUE`、`isSessionQueueSnapshot`、`shouldApplySnapshot`、`parseQueueAck`、`canEditQueuedMessage`、`canCancelQueuedMessage`、`canDeliverQueuedMessage`、`queuedMessageStatusLabel`、`queuePauseReasonLabel`
743
754
  - **页面协作**:`EmbeddedChat`、`EmbeddedChatOptions`、`CommandHandler`、`CommandEnvelope`、`InboundAction`、`InboundEnvelope`、`isCommandEnvelope`、`isInboundEnvelope`
744
- - **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`ContextProjectionData`、`ContextProjectionFields`、`ContextDisplayState`、`ContextGroupDisplayState`、`ContextAction`、`ContextSourceInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`PostChatFollowup`、`FinalArtifact`、`contextProjectionData`、`getContextDisplayState`、`getContextGroupDisplayState`、`groupAdjacentContextRuns`、`latestPostChatFollowup`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`SkillMentionAvailability`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
755
+ - **流式 wire 事件(自定义渲染用)**:`ContentDelta`、`ToolCallCreated`、`ToolCallArgumentsDelta`、`ToolResultDelta`、`ToolResultDone`、`LlmResponseDone`、`ChatEnd`、`SystemError`、`SessionStatusEvent`。它们描述 socket 上一帧帧到达的内容,只有自己接管流式拼装时才需要
756
+ - **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`ContextProjectionData`、`ContextProjectionFields`、`ContextDisplayState`、`ContextGroupDisplayState`、`ContextAction`、`ContextSourceInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`PostChatFollowup`、`FinalArtifact`、`ToolCallProjection`、`PendingQuestionRef`、`RunTiming`、`inferToolStatus`、`contextProjectionData`、`getContextDisplayState`、`getContextGroupDisplayState`、`groupAdjacentContextRuns`、`latestPostChatFollowup`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`SkillMentionAvailability`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
745
757
  - **MCP Apps 留档与卡片判定**:`McpAppData`、`isMcpAppData`、`McpAppContextState`、`McpAppContextUpdate`、`ToolUiCard`、`ToolUiCardContentType`、`InlineToolUiCardEntry`、`PreviewToolUiCardEntry`、`isToolUiCard`、`resolveToolUiCardContent`、`buildToolUiCardKey`、`isInternalStatusToolUiCard`、`isAppDevToolUiCard`、`collectInlineToolUiCards`、`collectPreviewToolUiCards`(`McpAppContextState` / `McpAppContextUpdate` 配合 `sessions.getMcpAppContext` / `sessions.updateMcpAppContext`:App 保存的状态在下一个新运行才进入模型上下文)
746
758
  - **Solution / 任务协议**:`Solution`、`SolutionAppField`、`SolutionAppState`、`SolutionAppUiConfig`、`SolutionRef`、`PublishedSolutionRef`、`ExistingSolutionRef`、`PreparedSolution`、`PreparedSolutionAsset`、`LayoutType`、`BizRole`、`TaskStatus`、`BackgroundTask`、`BackgroundTaskStopResult`
747
759
  - **Headless**:`HeadlessResource`、`RunOptions`、`RunResult`、`RunTrace`
@@ -99,6 +99,14 @@ export declare class BladeClient {
99
99
  private isSameBackendUrl;
100
100
  private shouldRefreshFor401;
101
101
  private hasExplicitBearerToken;
102
+ /**
103
+ * 等一次令牌刷新,但调用方中止请求时不再等下去。
104
+ *
105
+ * 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
106
+ * 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
107
+ * 刷新结束才轮得到。
108
+ */
109
+ private tryRefreshUnlessAborted;
102
110
  private tryRefresh;
103
111
  private resolveTokenForUrl;
104
112
  private resolveRestToken;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { BladeClient } from "./blade-client";
2
2
  export type { BladeClientOptions, UploadProgress } from "./blade-client";
3
3
  export { BladeApiError } from "./rest";
4
+ export { isEntryGoneError } from "./resources/sessions";
4
5
  export { PollingBackoff } from "./polling-backoff";
5
6
  export type { LoginOptions, LoginResult, TokenStorageMode } from "./auth-login";
6
7
  export { EMPTY_PLATFORM_ENDPOINTS, loadPlatformEndpoints, resolveServiceUrl, } from "./platform-endpoints";
@@ -40,13 +41,15 @@ export { PluginsResource } from "./resources/plugins";
40
41
  export type { PluginCatalogEntry } from "./resources/plugins";
41
42
  export type { SiteVisitsResource, SiteVisitsSummary } from "./resources/site-visits";
42
43
  export { hasChatRunEvent } from "./shared/projection";
43
- export type { CreateSessionRequest, AppCliAttachment, AppCliDefinition, FileEntry, ImportSessionOptions, PaginatedSessionsResult, GlobalSearchConversationResult, GlobalSearchFileResult, GlobalSearchResult, GlobalSearchResultItem, ResultFeedback, ResultFeedbackReason, HistoricalCommand, SessionContextStats, SessionHistory, SessionTurnsPage, ShareLinkResult, UploadFileEntry, UploadFilesOptions, } from "./resources/sessions";
44
+ export type { CreateSessionRequest, AppCliAttachment, AppCliDefinition, FileEntry, ImportSessionOptions, PaginatedSessionsResult, GlobalSearchConversationResult, GlobalSearchFileResult, GlobalSearchResult, GlobalSearchResultItem, ResultFeedback, ResultFeedbackReason, HistoricalCommand, SessionContextStats, SessionHistory, SessionTurnsPage, SessionTurnsQuery, SessionTurnIndex, SessionTurnIndexItem, ShareLinkResult, UploadFileEntry, UploadFilesOptions, } from "./resources/sessions";
44
45
  export type { ArchivedFileInfo, ArchivedToolCallInfo, ChatMessage, CompactionInfo, FileContentPart, ImageUrlContentPart, MemoryRefInfo, MessageContent, MessageContentPart, TextContentPart, ToolBridgeContent, ToolCallInfo, } from "./schemas/message";
46
+ export { inferToolStatus } from "./schemas/message";
47
+ export type { ChatEnd, ContentDelta, LlmResponseDone, SessionStatusEvent, SystemError, ToolCallArgumentsDelta, ToolCallCreated, ToolResultDelta, ToolResultDone, } from "./schemas/event";
45
48
  export { buildMessageContent, chatErrorForDisplay, contentPreview, extractTextAttachments, getFileParts, getImageParts, getTextContent, groupMessagesByLoop, isHiddenInternalMessage, normalizeMessageContent, transformSlashCommand, } from "./schemas/message-utils";
46
49
  export { contextProjectionData, getContextDisplayState, getContextGroupDisplayState, groupAdjacentContextRuns, } from "./schemas/context";
47
50
  export type { ContextAction, ContextDisplayState, ContextGroupDisplayState, ContextProjectionData, ContextProjectionFields, ContextSourceInfo, } from "./schemas/context";
48
51
  export type { ParsedTextAttachment, ParsedTextContext, SkillMentionAvailability, } from "./schemas/message-utils";
49
- export type { ContentBlock, FinalArtifact, MemoryRef, PatchEnvelope, PostChatFollowup, TurnProjection, } from "./schemas/projection";
52
+ export type { ContentBlock, FinalArtifact, MemoryRef, PatchEnvelope, PendingQuestionRef, PostChatFollowup, RunTiming, ToolCallProjection, TurnProjection, } from "./schemas/projection";
50
53
  export { latestPostChatFollowup } from "./schemas/projection-utils";
51
54
  export { buildToolUiCardKey, collectInlineToolUiCards, collectPreviewToolUiCards, isAppDevToolUiCard, isInternalStatusToolUiCard, isToolUiCard, resolveToolUiCardContent, } from "./shared/tool-ui-card";
52
55
  export type { InlineToolUiCardEntry, PreviewToolUiCardEntry, ToolUiCard, ToolUiCardContentType, } from "./shared/tool-ui-card";
@@ -54,7 +57,7 @@ export { prependOlder, replaceWindow } from "./session/history-page";
54
57
  export type { LiveRevisionState } from "./session/history-page";
55
58
  export { DEFAULT_REPLAY_SPEED } from "./schemas/session";
56
59
  export { SessionInfo, SessionStatus } from "./schemas/session";
57
- export type { ModeId, PrimarySkillParallelMode, PrimarySkillSnapshot, ReplayPreview, ReplaySpeed, ReplayState, SessionDetail, SessionPortMapping, PublishedSolutionRef, TemplateId, } from "./schemas/session";
60
+ export type { ModeId, PrimarySkillParallelMode, PrimarySkillSnapshot, PrimarySkillStageSpec, PrimarySkillStepSpec, PublishedSolutionRef, ReplayPreview, ReplaySpeed, ReplayState, SessionDetail, SessionPortMapping, SessionSearchMatch, SkillEditorTemplateId, TemplateId, } from "./schemas/session";
58
61
  export { LayoutType } from "./schemas/solution";
59
62
  export type { BizRole, Solution, SolutionAppField, SolutionAppState, SolutionAppUiConfig, } from "./schemas/solution";
60
63
  export { Task, TaskStatus } from "./schemas/task";
package/dist/index.js CHANGED
@@ -2596,6 +2596,10 @@ function findLatestAssistant(turns, loopId) {
2596
2596
  }
2597
2597
 
2598
2598
  // src/resources/sessions.ts
2599
+ function isEntryGoneError(error) {
2600
+ const status = error?.status;
2601
+ return status === 404 || status === 409;
2602
+ }
2599
2603
  function toCreateSessionPayload(request) {
2600
2604
  const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
2601
2605
  return {
@@ -2932,18 +2936,36 @@ var SessionsResource = class {
2932
2936
  );
2933
2937
  return projectHistory(entries);
2934
2938
  }
2935
- async getSessionTurnsPage(sessionId, options = {}) {
2939
+ async getSessionTurnsPage(sessionId, options = {}, init = {}) {
2936
2940
  const query = new URLSearchParams();
2937
2941
  if (options.limit !== void 0) query.set("limit", String(options.limit));
2938
2942
  if (options.before !== void 0) query.set("before", options.before);
2943
+ if (options.fromEntryId !== void 0) query.set("from_entry_id", options.fromEntryId);
2939
2944
  const suffix = query.size ? `?${query}` : "";
2940
- const page = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`);
2945
+ const page = await this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`, init);
2941
2946
  return {
2942
2947
  turns: page.turns,
2943
2948
  nextBefore: page.next_before,
2944
2949
  currentMode: page.current_mode
2945
2950
  };
2946
2951
  }
2952
+ /**
2953
+ * 当前分支上可导航的用户发言目录(消息导航侧栏)。
2954
+ *
2955
+ * 只回 id、标题和时间:侧栏要列的是**整条会话**的发言,而窗口外的发言 id 只能
2956
+ * 从服务端拿;消息体仍然走 `/turns`。
2957
+ */
2958
+ async getSessionTurnIndex(sessionId) {
2959
+ const index = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-index`);
2960
+ return {
2961
+ sessionId: index.session_id,
2962
+ items: index.items.map((item) => ({
2963
+ entryId: item.entry_id,
2964
+ preview: item.preview,
2965
+ createdAt: item.created_at
2966
+ }))
2967
+ };
2968
+ }
2947
2969
  getMcpAppContext(sessionId, sourceId, init) {
2948
2970
  return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, init);
2949
2971
  }
@@ -4209,12 +4231,12 @@ function orderTurns(turns) {
4209
4231
  return turns;
4210
4232
  }
4211
4233
  function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
4212
- return reconcileUserTurns(currentTurns, incomingTurns, true);
4234
+ return reconcileUserTurns(currentTurns, incomingTurns, true, false);
4213
4235
  }
4214
4236
  function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
4215
- return reconcileUserTurns(currentTurns, incomingTurns, false);
4237
+ return reconcileUserTurns(currentTurns, incomingTurns, false, true);
4216
4238
  }
4217
- function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback) {
4239
+ function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback, keepUnclaimedOptimistic) {
4218
4240
  const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
4219
4241
  const optimisticUserTurns = currentTurns.filter(
4220
4242
  (turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
@@ -4242,7 +4264,13 @@ function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback) {
4242
4264
  }
4243
4265
  return turn;
4244
4266
  });
4245
- return claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
4267
+ const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
4268
+ if (!keepUnclaimedOptimistic) return result;
4269
+ const resultIds = new Set(result.map((turn) => turn.turn_id));
4270
+ const unclaimedOptimisticTurns = optimisticUserTurns.filter(
4271
+ (turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
4272
+ );
4273
+ return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
4246
4274
  }
4247
4275
  function isUserConfirmation(turn) {
4248
4276
  return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
@@ -4350,7 +4378,7 @@ function upsertTurn(state, turn) {
4350
4378
  } else {
4351
4379
  existing.push(nextTurn);
4352
4380
  }
4353
- return withTurns(state, existing);
4381
+ return withTurns({ ...state, turns: existing }, existing);
4354
4382
  }
4355
4383
  function applyTurnPatch(state, patch) {
4356
4384
  const turn = patch.data.turn;
@@ -4487,6 +4515,10 @@ var AgentSession = class _AgentSession {
4487
4515
  subscriptionQueue = Promise.resolve();
4488
4516
  replaceQueue = Promise.resolve();
4489
4517
  timelineEpoch = 0;
4518
+ /** 定位请求的流水号:被后一次选择取代的那次定位不再提交窗口(见 locateEntry)。 */
4519
+ locateToken = 0;
4520
+ /** 在途那次定位的请求句柄。**只**服务定位:权威刷新不共用它(见 cancelPendingLocate)。 */
4521
+ locateAbort = null;
4490
4522
  pagingGeneration = 0;
4491
4523
  liveRevision = 0;
4492
4524
  modeRevision = 0;
@@ -4522,11 +4554,19 @@ var AgentSession = class _AgentSession {
4522
4554
  seenToolCallIds = /* @__PURE__ */ new Set();
4523
4555
  finishedToolCallIds = /* @__PURE__ */ new Set();
4524
4556
  emittedMessageTurnIds = /* @__PURE__ */ new Set();
4525
- constructor(sessionId, runtime, connection) {
4557
+ constructor(sessionId, runtime, connection, options = {}) {
4526
4558
  this.sessionId = sessionId;
4527
4559
  this.runtime = runtime;
4528
4560
  this.state = { ...createInitialSessionState(sessionId), connection };
4561
+ this.isStatic = options.staticHistory === true;
4529
4562
  }
4563
+ /**
4564
+ * 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
4565
+ *
4566
+ * 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
4567
+ * 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
4568
+ */
4569
+ isStatic;
4530
4570
  /** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
4531
4571
  static fromTurns(sessionId, turns) {
4532
4572
  const readonlyError = () => {
@@ -4539,6 +4579,8 @@ var AgentSession = class _AgentSession {
4539
4579
  emitWithAck: async () => readonlyError(),
4540
4580
  ensureConnected: readonlyError,
4541
4581
  fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
4582
+ // 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
4583
+ fetchTurnIndex: async () => ({ sessionId, items: [] }),
4542
4584
  fetchCommands: async () => [],
4543
4585
  // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
4544
4586
  // 不会跑到这里,但保持只读语义一致。
@@ -4550,7 +4592,8 @@ var AgentSession = class _AgentSession {
4550
4592
  attachAppCli: async () => readonlyError(),
4551
4593
  onDispose: () => true
4552
4594
  },
4553
- "connected"
4595
+ "connected",
4596
+ { staticHistory: true }
4554
4597
  );
4555
4598
  session.state = {
4556
4599
  ...withTurns(session.state, turns),
@@ -4578,7 +4621,7 @@ var AgentSession = class _AgentSession {
4578
4621
  const epoch = this.timelineEpoch;
4579
4622
  const generation = this.pagingGeneration;
4580
4623
  this.update((state) => ({ ...state, loadingOlder: true }));
4581
- const request = this.runtime.fetchTurnsPage(this.sessionId, before).then((page) => {
4624
+ const request = this.runtime.fetchTurnsPage(this.sessionId, { before }).then((page) => {
4582
4625
  if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
4583
4626
  options.beforeCommit?.();
4584
4627
  this.update((state) => ({
@@ -4586,10 +4629,11 @@ var AgentSession = class _AgentSession {
4586
4629
  nextBefore: page.nextBefore,
4587
4630
  loadingOlder: false
4588
4631
  }));
4589
- }).catch((error) => {
4632
+ }).catch(async (error) => {
4590
4633
  if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
4591
- this.timelineEpoch += 1;
4592
- return this.replaceFromHistory();
4634
+ this.invalidateTimeline();
4635
+ await this.replaceFromHistory();
4636
+ return;
4593
4637
  }
4594
4638
  }).finally(() => {
4595
4639
  if (this.olderPending !== request) return;
@@ -4599,6 +4643,64 @@ var AgentSession = class _AgentSession {
4599
4643
  this.olderPending = request;
4600
4644
  return request;
4601
4645
  }
4646
+ /**
4647
+ * 定位到一条窗口外的历史发言:请服务端返回"从这条发言到最新"的窗口并整窗替换。
4648
+ *
4649
+ * 返回 false 表示这次定位没有落地(请求失败,或期间 timeline 被重写、会话已释放),
4650
+ * 当前窗口保持不变,调用方可以原样重试。已加载的发言不要走这里——宿主直接滚动,
4651
+ * 一次网络请求都不该发。
4652
+ */
4653
+ async locateEntry(entryId) {
4654
+ const token = ++this.locateToken;
4655
+ const controller = new AbortController();
4656
+ this.locateAbort?.abort();
4657
+ this.locateAbort = controller;
4658
+ try {
4659
+ return await this.replaceFromHistory({
4660
+ suppressErrors: false,
4661
+ fromEntryId: entryId,
4662
+ signal: controller.signal,
4663
+ isStale: () => this.locateToken !== token
4664
+ });
4665
+ } catch {
4666
+ return false;
4667
+ } finally {
4668
+ if (this.locateAbort === controller) this.locateAbort = null;
4669
+ }
4670
+ }
4671
+ /**
4672
+ * 取消在途定位:用户改选了另一条发言(哪怕那条已经加载、不需要请求),
4673
+ * 先前那次定位既不该再落地,也不该继续占着替换队列——排队中的下一次定位
4674
+ * 不然要等它跑完(用户看到的是"点了新目标一直转圈")。
4675
+ *
4676
+ * **只**中止定位自己的请求:权威刷新(运行结束 / 重连补数)不共用这个 controller,
4677
+ * 否则"运行刚结束时点一下侧栏"就会把终态对账一起中止,流式与历史对不上。
4678
+ */
4679
+ cancelPendingLocate() {
4680
+ this.locateToken += 1;
4681
+ this.locateAbort?.abort();
4682
+ this.locateAbort = null;
4683
+ }
4684
+ /**
4685
+ * timeline 被整体改写(rewind / checkout / 释放):推进纪元,并把在途定位请求一并中止
4686
+ * ——它已经属于旧 timeline,却还占着替换队列,会让改写后的权威窗口排在后面等它跑完。
4687
+ * 作废号仍然保留:中止只是让它更早结束。
4688
+ */
4689
+ invalidateTimeline() {
4690
+ this.timelineEpoch += 1;
4691
+ this.locateAbort?.abort();
4692
+ this.locateAbort = null;
4693
+ }
4694
+ /**
4695
+ * 拉取当前分支的用户发言目录(消息导航侧栏用)。
4696
+ *
4697
+ * 不在这里缓存:什么时候该刷新只有调用方知道(会话切换、历史被替换、新发言),
4698
+ * 缓存只会让旧条目在 rewind 之后继续可点。
4699
+ */
4700
+ async loadTurnIndex() {
4701
+ const index = await this.runtime.fetchTurnIndex(this.sessionId);
4702
+ return index.items;
4703
+ }
4602
4704
  /** 订阅状态变化。返回取消订阅函数。 */
4603
4705
  subscribe(listener) {
4604
4706
  this.listeners.add(listener);
@@ -4847,7 +4949,7 @@ var AgentSession = class _AgentSession {
4847
4949
  whatif: options.whatif,
4848
4950
  replay_decision: options.replayDecision,
4849
4951
  app_context: appContext,
4850
- kb_ids: options.kbIds && options.kbIds.length > 0 ? options.kbIds : void 0,
4952
+ kb_ids: options.kbIds,
4851
4953
  // 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
4852
4954
  queue_when_running: options.queueWhenRunning === true
4853
4955
  };
@@ -4912,10 +5014,8 @@ var AgentSession = class _AgentSession {
4912
5014
  this.pendingReplayMode = void 0;
4913
5015
  this.pendingReplayKbIds = void 0;
4914
5016
  this.update((current) => {
4915
- let next = optimisticTurnId ? withTurns(
4916
- current,
4917
- current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
4918
- ) : current;
5017
+ const filtered = optimisticTurnId ? current.turns.filter((turn) => turn.turn_id !== optimisticTurnId) : current.turns;
5018
+ let next = optimisticTurnId ? withTurns({ ...current, turns: filtered }, filtered) : current;
4919
5019
  const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
4920
5020
  if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
4921
5021
  const askAnswers = { ...next.askAnswers };
@@ -5001,7 +5101,7 @@ var AgentSession = class _AgentSession {
5001
5101
  if (this.disposed) return;
5002
5102
  if (!this.runtime.onDispose(this)) return;
5003
5103
  this.disposed = true;
5004
- this.timelineEpoch += 1;
5104
+ this.invalidateTimeline();
5005
5105
  this.invalidateOlderPaging();
5006
5106
  this.state = { ...this.state, nextBefore: null, loadingOlder: false };
5007
5107
  this.cancelPatchFlush();
@@ -5036,7 +5136,6 @@ var AgentSession = class _AgentSession {
5036
5136
  }
5037
5137
  /** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
5038
5138
  _rejoinAfterReconnect() {
5039
- this.timelineEpoch += 1;
5040
5139
  this.invalidateOlderPaging();
5041
5140
  this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5042
5141
  const reconnectLiveRevision = this.liveRevision;
@@ -5054,7 +5153,7 @@ var AgentSession = class _AgentSession {
5054
5153
  this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
5055
5154
  }
5056
5155
  await Promise.all([
5057
- this.replaceFromHistory({
5156
+ this.refreshAuthoritativeHistory({
5058
5157
  liveRevisionAtStart: reconnectLiveRevision,
5059
5158
  modeRevisionAtStart: reconnectModeRevision
5060
5159
  }),
@@ -5216,7 +5315,7 @@ var AgentSession = class _AgentSession {
5216
5315
  }
5217
5316
  /** @internal */
5218
5317
  _handleRewind(checkpointId) {
5219
- this.timelineEpoch += 1;
5318
+ this.invalidateTimeline();
5220
5319
  this.invalidateOlderPaging();
5221
5320
  this.update((s) => ({
5222
5321
  ...s,
@@ -5244,7 +5343,7 @@ var AgentSession = class _AgentSession {
5244
5343
  message,
5245
5344
  mode,
5246
5345
  replay_decision: decision,
5247
- kb_ids: kbIds && kbIds.length > 0 ? kbIds : void 0
5346
+ kb_ids: kbIds
5248
5347
  },
5249
5348
  3e4
5250
5349
  ).then((response) => {
@@ -5314,7 +5413,7 @@ var AgentSession = class _AgentSession {
5314
5413
  this.update((s) => ({ ...s, status: "running", isStreaming: true }));
5315
5414
  } else if (this.state.isStreaming) {
5316
5415
  this.update((s) => ({ ...s, isStreaming: false }));
5317
- void this.replaceFromHistory({ liveRevisionAtStart: liveRevisionAtJoin });
5416
+ void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
5318
5417
  }
5319
5418
  }
5320
5419
  // ---------------------------------------------------------------- 内部实现
@@ -5335,7 +5434,7 @@ var AgentSession = class _AgentSession {
5335
5434
  completeChat(status) {
5336
5435
  this.finishStreaming(status);
5337
5436
  this.recentChatEndAt = Date.now();
5338
- void this.replaceFromHistory();
5437
+ void this.refreshAuthoritativeHistory();
5339
5438
  this.emitter.emit("chatEnd", { status });
5340
5439
  }
5341
5440
  markTurnFresh(turnId) {
@@ -5519,8 +5618,32 @@ var AgentSession = class _AgentSession {
5519
5618
  this.update((s) => addErrorMessage(s, OOM_MESSAGE));
5520
5619
  this.emitter.emit("sandboxOom", { toolCallId: key });
5521
5620
  }
5621
+ /**
5622
+ * 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
5623
+ *
5624
+ * 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
5625
+ * 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
5626
+ * 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
5627
+ * 回落到最新一页。
5628
+ */
5629
+ refreshAuthoritativeHistory(options = {}) {
5630
+ let entryGone = false;
5631
+ const locateTokenAtStart = this.locateToken;
5632
+ return this.replaceFromHistory({
5633
+ ...options,
5634
+ // 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
5635
+ fromEntryId: () => this.state.turns[0]?.turn_id,
5636
+ onError: (error) => {
5637
+ entryGone = isEntryGoneError(error);
5638
+ }
5639
+ }).then((located) => {
5640
+ if (located || !entryGone) return located;
5641
+ if (this.locateToken !== locateTokenAtStart) return false;
5642
+ return this.replaceFromHistory(options);
5643
+ });
5644
+ }
5522
5645
  replaceFromHistory(options = {}) {
5523
- if (this.disposed) return Promise.resolve();
5646
+ if (this.disposed) return Promise.resolve(false);
5524
5647
  const operationEpoch = this.timelineEpoch;
5525
5648
  const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
5526
5649
  const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
@@ -5528,13 +5651,35 @@ var AgentSession = class _AgentSession {
5528
5651
  this.replaceInFlight += 1;
5529
5652
  const operation = async () => {
5530
5653
  let previousNextBefore = null;
5654
+ const restoreCursor = () => {
5655
+ if (this.disposed || previousNextBefore === null) return;
5656
+ this.update((state) => ({
5657
+ ...state,
5658
+ nextBefore: previousNextBefore,
5659
+ loadingOlder: false
5660
+ }));
5661
+ };
5531
5662
  try {
5532
- if (this.disposed || operationEpoch !== this.timelineEpoch) return;
5663
+ if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
5664
+ if (options.isStale?.()) return false;
5533
5665
  this.invalidateOlderPaging();
5534
5666
  previousNextBefore = this.state.nextBefore;
5535
5667
  this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
5536
- const page = await this.runtime.fetchTurnsPage(this.sessionId);
5537
- if (this.disposed || operationEpoch !== this.timelineEpoch) return;
5668
+ const signal = options.signal;
5669
+ const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
5670
+ const page = await this.runtime.fetchTurnsPage(
5671
+ this.sessionId,
5672
+ fromEntryId ? { fromEntryId } : void 0,
5673
+ signal ? { signal } : void 0
5674
+ );
5675
+ if (this.disposed || operationEpoch !== this.timelineEpoch) {
5676
+ restoreCursor();
5677
+ return false;
5678
+ }
5679
+ if (options.isStale?.()) {
5680
+ restoreCursor();
5681
+ return false;
5682
+ }
5538
5683
  if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
5539
5684
  this.update((state) => {
5540
5685
  const next = withTurns(
@@ -5553,7 +5698,9 @@ var AgentSession = class _AgentSession {
5553
5698
  });
5554
5699
  this.pruneOptimisticLiveRevisions();
5555
5700
  this.projectionBuilder._seedTurns(this.state.turns);
5701
+ return true;
5556
5702
  } catch (error) {
5703
+ options.onError?.(error);
5557
5704
  if (!this.disposed && operationEpoch === this.timelineEpoch) {
5558
5705
  this.update((state) => ({
5559
5706
  ...state,
@@ -5562,6 +5709,7 @@ var AgentSession = class _AgentSession {
5562
5709
  }));
5563
5710
  }
5564
5711
  if (options.suppressErrors === false) throw error;
5712
+ return false;
5565
5713
  } finally {
5566
5714
  this.replaceInFlight -= 1;
5567
5715
  }
@@ -5836,7 +5984,8 @@ var SessionHub = class {
5836
5984
  });
5837
5985
  }),
5838
5986
  ensureConnected: () => this.ensureConnected(),
5839
- fetchTurnsPage: (sessionId, before) => this.client.sessions.getSessionTurnsPage(sessionId, { before }),
5987
+ fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
5988
+ fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
5840
5989
  fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
5841
5990
  fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
5842
5991
  updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
@@ -6309,7 +6458,7 @@ var BladeClient = class {
6309
6458
  signal: init.signal
6310
6459
  });
6311
6460
  if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
6312
- const refreshed = await this.tryRefresh();
6461
+ const refreshed = await this.tryRefreshUnlessAborted(init.signal);
6313
6462
  if (refreshed) {
6314
6463
  return this.fetch(method, path, init, true);
6315
6464
  }
@@ -6363,6 +6512,27 @@ var BladeClient = class {
6363
6512
  hasExplicitBearerToken() {
6364
6513
  return Boolean(this.resolveRestToken());
6365
6514
  }
6515
+ /**
6516
+ * 等一次令牌刷新,但调用方中止请求时不再等下去。
6517
+ *
6518
+ * 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
6519
+ * 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
6520
+ * 刷新结束才轮得到。
6521
+ */
6522
+ async tryRefreshUnlessAborted(signal) {
6523
+ if (!signal) return this.tryRefresh();
6524
+ if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
6525
+ let onAbort;
6526
+ const aborted = new Promise((_resolve, reject) => {
6527
+ onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
6528
+ signal.addEventListener("abort", onAbort, { once: true });
6529
+ });
6530
+ try {
6531
+ return await Promise.race([this.tryRefresh(), aborted]);
6532
+ } finally {
6533
+ if (onAbort) signal.removeEventListener("abort", onAbort);
6534
+ }
6535
+ }
6366
6536
  async tryRefresh() {
6367
6537
  if (this.refreshPromise) {
6368
6538
  return this.refreshPromise;
@@ -6618,7 +6788,7 @@ function resolveServiceUrl(endpoints, name, path = "") {
6618
6788
 
6619
6789
  // src/version.ts
6620
6790
  var SDK_NAME = "agent-client";
6621
- var SDK_VERSION = true ? "2610.0.0-beta.70" : "1.1.1";
6791
+ var SDK_VERSION = true ? "2610.0.0-beta.72" : "1.1.1";
6622
6792
 
6623
6793
  // src/commands/protocol.ts
6624
6794
  function isCommandEnvelope(value) {
@@ -6771,8 +6941,10 @@ export {
6771
6941
  groupAdjacentContextRuns,
6772
6942
  groupMessagesByLoop,
6773
6943
  hasChatRunEvent,
6944
+ inferToolStatus,
6774
6945
  isAppDevToolUiCard,
6775
6946
  isCommandEnvelope,
6947
+ isEntryGoneError,
6776
6948
  isHiddenInternalMessage,
6777
6949
  isInboundEnvelope,
6778
6950
  isInternalStatusToolUiCard,