@sema-agent/client-core 0.30.5 → 0.30.7

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.
@@ -358,23 +358,119 @@ export function getBgTerminalFacts(taskId) {
358
358
  }
359
359
  // ── bg 子代 → spawn 它的 leader run(server 1.244 subagentOutput 读面的寻址元组)──────────────
360
360
  // GET /v1/runs/:runId/subagents/:handle/output 的 runId = spawn 该子代的 run(registry access 从
361
- // run 行推导,[1491]①)。来源两路:fleet 行 parentId(出生帧即带)+ bg_notification.parentTaskId
361
+ // run 行推导,[1491]①)。来源三路:fleet 行 parentId(出生帧即带)+ bg_notification.parentTaskId
362
+ // + `task_progress` tick(#242 批 2 新腿,见下)。
363
+ //
364
+ // 🔴 #242 批 2(A-028.6 收编,2026-08-15):本表**收编壳侧 `subagentOwnerLedger`**,成为
365
+ // 「这一行子代属于哪一条引擎 run」的**唯一台账**。收编前同一个问题有两份表、两个相反答案:
366
+ // · 壳表(cli `src/sema/subagentOwnerLedger.ts`,cap 256)由 `task_progress` tick 喂,
367
+ // 取址缺席时**诚实缺席**(指名了行就绝不回落到「此刻在飞的那条」);
368
+ // · 本表由 fleet 行帧(`fleet/fleetLedger.ts:394`)+ bg 通知(`:496`)喂,三处消费方一律
369
+ // `getBgParentRun(x) ?? activeEngineRunId()` 无条件回落。
370
+ // 两表覆盖面**互补**(壳表只覆盖发 tick 的子代;fleet-row lane 子代零 tick,只有本表覆盖),
371
+ // 所以合表不是二选一,是把两条 lane 的事实并进同一个答案。
372
+ // 键域两侧本就同域,合表零改键:fleet 腿写 `rowIdTail(row.id)` = 裸引擎 taskId、通知腿写
373
+ // `n.taskId` = registry handle(同一裸 id)、tick 腿写 `task_progress.taskId`。
374
+ // ⚠️ 那三处 `?? activeEngineRunId()` 回落口径**本批刻意不动**(engineSubagentTail.ts:172 /
375
+ // engineTaskHandleWire.ts:69 / engineSubagentOutput.ts:82),候 [4000] Q3 三端表态。
376
+ //
377
+ // 🔴 写入纪律(壳表头注 :20-22 的教训**原文搬**):宿主 run **必须由持有 stream-local 值的
378
+ // 调用方显式传入**,本模块绝不从进程级 `activeEngineRunId()` 推断 —— 那个值被并发的
379
+ // main/fork 流互相覆写、又被每个 query 的 finally 无条件清空,推断出来的宿主可能是**别的
380
+ // run**,而**错值比缺席更坏**(消费端的门只查「有没有值」,分辨不出错值:continue 会打到
381
+ // 不相干的 run,甚至叫醒那条 run 里的同名子代)。取不到 ⇒ 什么都不记。
382
+ //
383
+ // 🔴 有界:键来自 wire(不可信),无界表 = 引擎乱发 taskId 就能吃掉宿主内存。上限取**两表
384
+ // 上限之和**(壳表 256 + 本表原 `MAX_TASKS*2`=64)—— 合表**不许缩窄既有保障**:收编前两条
385
+ // lane 各有各的池子,能同时记 256 条 tick + 64 条 fleet;若按 256 钉,「tick 池已满 + 再来一条
386
+ // fleet 行」当场淘汰一条 tick,那条子代的 resume 入口凭空消失,而收编前它还在(codex 复审
387
+ // medium 实证形)。两读口的 LRU touch 也会让高频 fleet 行持续挤压 tick 历史,同一条道理。
388
+ // 满了按 LRU 淘汰,淘汰后果只是那一行退回「不知道宿主 run」= 诚实缺席,不是错值。
389
+ const MAX_BG_PARENT_RUNS = 320;
362
390
  const bgParentRun = new Map();
363
- export function recordBgParentRun(taskId, runId) {
364
- if (!taskId || !runId || taskId === runId)
391
+ /**
392
+ * fleet 行帧 / bg 通知腿的登记口(既有两写点)
393
+ * `sessionId` 是 #242 批 2 additive 补位:缺席则不落键(读面据此判「不带 `?session=`」)。
394
+ */
395
+ export function recordBgParentRun(taskId, runId, sessionId) {
396
+ recordSubagentOwner(taskId, runId, sessionId);
397
+ }
398
+ /**
399
+ * 「这一行子代的宿主 run」统一登记口(**显式传值**,零推断,见本段头注)。
400
+ * `runId` 缺席/空串 ⇒ 什么都不记(宁可「不知道」也不记一个错的宿主);`taskId === runId`
401
+ * 的退化自指映射同样拒收(顶层 run 行不是任何人的子代)。
402
+ *
403
+ * 🔴 **`sessionId` 的缺席不覆盖在场**(合表的承重口径,#242 批 2 自查抓出):喂本表的三条腿里
404
+ * **只有 tick 腿带 session**(它在开流时一次性捕获),fleet 行帧腿与 bg 通知腿都不带。合表前
405
+ * 两表各记各的,合表后若让「后到的无 session 写」直接盖掉整条记录,tick 捕获的会话就会被抹掉,
406
+ * resume 退回「此刻的会话」—— 那正是对抗复审轮5 M1 要防的错组合(旧 runId + 新 sessionId ⇒
407
+ * session-bound run 上 fail-closed 404)。
408
+ * 口径:**同一 `runId` 上缺席不覆盖在场**(带值的写照常赢);`runId` 真变了 ⇒ 旧 session 必须丢
409
+ * (那是另一条宿主 run 的会话,留着就是错组合)。
410
+ */
411
+ export function recordSubagentOwner(taskId, runId, sessionId) {
412
+ if (!taskId || typeof runId !== 'string' || runId === '' || taskId === runId)
365
413
  return;
366
- if (bgParentRun.size >= MAX_TASKS * 2 && !bgParentRun.has(taskId)) {
414
+ if (bgParentRun.size >= MAX_BG_PARENT_RUNS && !bgParentRun.has(taskId)) {
367
415
  const oldest = bgParentRun.keys().next().value;
368
416
  if (oldest !== undefined)
369
417
  bgParentRun.delete(oldest);
370
418
  }
419
+ const prior = bgParentRun.get(taskId);
420
+ const kept = typeof sessionId === 'string' && sessionId !== ''
421
+ ? sessionId
422
+ : (prior?.runId === runId ? prior.sessionId : undefined);
371
423
  bgParentRun.delete(taskId); // LRU touch(F11):活跃映射不被容量淘汰挤掉 → 读面退错 runId
372
- bgParentRun.set(taskId, runId);
424
+ bgParentRun.set(taskId, {
425
+ runId,
426
+ ...(kept !== undefined && kept !== '' ? { sessionId: kept } : {}),
427
+ });
428
+ }
429
+ /**
430
+ * `task_progress` 腿的登记口(#242 批 2 ①:收编前本表**零 task_progress 腿**,已直证)。
431
+ *
432
+ * 一条帧进,认得就记,认不得就什么都不做(**绝不改帧**)。只认 `task_progress` —— 引擎侧子代的
433
+ * 累计 tick,带子代自己的 `taskId`。
434
+ *
435
+ * 🔴 **`ownerRunId` / `ownerSessionId` 必须由调用方显式喂**,本函数**不许**从
436
+ * `activeEngineRunId()` 推断(壳 `subagentOwnerLedger.ts:20-22` 对抗复审轮3 H2 实撞的教训:
437
+ * 进程级值被并发 main/fork 流互相覆写 + 每个 query 的 finally 清空 ⇒ run A 的 tick 可能被
438
+ * 登记到 run B 名下,而错值比缺席更坏)。登记点必须在**持有 stream-local run id 的那个闭包**
439
+ * 里(壳 = `liveClient.ts` 的 `stampSubagentOwners`),把不可变的值传进来。
440
+ * 🔴 `ownerSessionId` 同理**开流时一次性捕获**,不逐帧现读:逐帧现读会让 `/clear`、resume、
441
+ * 会话切换之后晚到的旧流 tick 被登记成 `{旧 runId, 新 sessionId}`,这对组合去 resume 会带着
442
+ * 错 session 打旧 run,fail-closed 404。
443
+ */
444
+ export function recordSubagentOwnerFromProgress(ev, ownerRunId, ownerSessionId) {
445
+ if (typeof ev !== 'object' || ev === null)
446
+ return;
447
+ const o = ev;
448
+ if (o.type !== 'task_progress')
449
+ return;
450
+ if (typeof o.taskId !== 'string' || o.taskId === '')
451
+ return;
452
+ if (typeof ownerRunId !== 'string' || ownerRunId === '')
453
+ return;
454
+ recordSubagentOwner(o.taskId, ownerRunId, ownerSessionId);
373
455
  }
374
456
  export function getBgParentRun(taskId) {
375
457
  touchLru(bgParentRun, taskId);
458
+ return bgParentRun.get(taskId)?.runId;
459
+ }
460
+ /**
461
+ * 整条宿主记录(runId + 登记时会话)。undefined = **诚实不知道**,调用方渲 no-run 出路 /
462
+ * 不渲入口 —— 绝不在这里回落成「此刻在飞的那条 run」([anchor-on-the-deciding-quantity])。
463
+ */
464
+ export function getBgParentRunOwner(taskId) {
465
+ if (typeof taskId !== 'string' || taskId === '')
466
+ return undefined;
467
+ touchLru(bgParentRun, taskId);
376
468
  return bgParentRun.get(taskId);
377
469
  }
470
+ /** 测试钩:清宿主台账(module 级单例,同进程多组断言必须能清)。 */
471
+ export function __resetSubagentOwnerLedgerForTests() {
472
+ bgParentRun.clear();
473
+ }
378
474
  // ── 本壳自己发起过的引擎 run 台账([1498]④ 帧级校验换锚)────────────────────────────────────
379
475
  // 喂给:engineToolDetach.setActiveEngineTaskId(liveClient 从 run_started 帧绑定,本壳每个交互
380
476
  // turn 的 run 必经)。与 bgParentRun 的关键区别:bgParentRun 由 fleet 行/通知帧喂(≤1.244 事实
@@ -1,5 +1,5 @@
1
- import type { WorkflowRun } from '@sema-agent/sdk';
2
- import type { WorkflowRunState } from './workflowMonitor.js';
1
+ import type { WorkflowRun, WorkflowStreamEvent } from '@sema-agent/sdk';
2
+ import type { WorkflowRunState, WorkflowToolCall } from './workflowMonitor.js';
3
3
  /** The live controller the host drives — a projected-snapshot reader + diagnostics + teardown. */
4
4
  export interface LiveWorkflowController {
5
5
  /** the latest projected run state, or null (not yet resolved / degraded 501-404 / empty). */
@@ -63,6 +63,66 @@ export interface LiveWorkflowConfig {
63
63
  * tolerates absent/permissive fields and never throws (the graceful-degrade contract lives above, in the
64
64
  * source loop; this just maps a well-formed run). Exported for the mock-parity unit cross-check. */
65
65
  export declare function projectWorkflowRun(run: WorkflowRun): WorkflowRunState;
66
+ /** 台账里的一个 agent(读口产物,只读快照;缺席 = 不知道,绝不渲成 0/空)。 */
67
+ export interface WorkflowActivityAgentView {
68
+ /** `ctx.agent` 调用的稳定身份(SSE 主键);老引擎/无 callKey 的帧上缺席。 */
69
+ callKey?: string;
70
+ label: string;
71
+ /** `agent_end.toolCalls` 权威;缺席时 = `agent_activity` 的 start 相位计数(**下界**)。 */
72
+ toolCalls: number;
73
+ /** per-toolCallId 折好的 call 序(上限 {@link MAX_WORKFLOW_ACTIVITY_CALLS},超限丢最老)。 */
74
+ activity: WorkflowToolCall[];
75
+ lastToolName?: string;
76
+ lastToolSummary?: string;
77
+ /** `agent_start.model` —— **请求词原样**(不是生效模型)。 */
78
+ requestedModel?: string;
79
+ /** `agent_end.modelResolved` —— 实跑模型;core 的 workflow 记录从不折入它 ⇒ GET 结构上到不了。 */
80
+ resolvedModel?: string;
81
+ /** 尾条 beat 的 ts。 */
82
+ lastProgressAt?: number;
83
+ }
84
+ /** 台账读口产物。`byLabel` **已排除**归属不可判的 label —— 端侧零重名逻辑。 */
85
+ export interface WorkflowActivityLedgerView {
86
+ workflowId: string;
87
+ byCallKey: ReadonlyMap<string, WorkflowActivityAgentView>;
88
+ byLabel: ReadonlyMap<string, WorkflowActivityAgentView>;
89
+ /** 同 label 多个非 replay 的 agent_start ⇒ 归属不可判(诊断面;`byLabel` 已按它过滤)。 */
90
+ ambiguousLabels: ReadonlySet<string>;
91
+ /** 流已收口(run 终态 / 显式 stop)——数据保留只读,不再消费。 */
92
+ closed: boolean;
93
+ /** 已折入的帧数(反空转:0 = 这条台账一帧都没收到,「全都没有」不构成证据)。 */
94
+ frames: number;
95
+ /** 最近一次 SSE 错误的人话;从没出过错 = null。**诊断面**,不参与归属判定。 */
96
+ lastError: string | null;
97
+ }
98
+ /** 预热口/单流化源共用的连线配置(与 {@link LiveWorkflowConfig} 同域,workflowId 必填)。 */
99
+ export interface WorkflowActivityLedgerConfig {
100
+ baseUrl: string;
101
+ authToken: string | {
102
+ mode: 'same-origin-relay';
103
+ };
104
+ principal: string | undefined;
105
+ workflowId: string;
106
+ fetchImpl?: typeof fetch;
107
+ }
108
+ /** 台账读口:没有该 id 的台账 ⇒ **null**(不是空视图 —— 「没开过账」与「开了账但一帧没收到」
109
+ * 是两件事,端侧据此分辨)。产物只读,按 frames/closed 缓存。 */
110
+ export declare function readWorkflowActivityLedger(workflowId: string): WorkflowActivityLedgerView | null;
111
+ /**
112
+ * **预热口**(幂等):fleet 流上见到 running workflow 行就调 —— 从 workflow 起跑积累工具事实,
113
+ * monitor 无论何时打开都有自那时起的完整台账。已有台账(哪怕已收口)即早退,**绝不开第二条流**。
114
+ */
115
+ export declare function ensureWorkflowActivityLedger(cfg: WorkflowActivityLedgerConfig): void;
116
+ /** fleet 行终态/remove 的收口口(空流收口是主径;这里防重连定时器竞态)。数据保留只读。 */
117
+ export declare function stopWorkflowActivityLedger(workflowId: string): void;
118
+ /** 测试钩:清空全部台账(abort 在飞流)。 */
119
+ export declare function resetWorkflowActivityLedgers(): void;
120
+ /**
121
+ * 测试钩:**不起 SSE 流**,直接把一帧喂进(缺则新建)指定 run 的台账 —— 折叠规则走产品那一份
122
+ * `foldWorkflowActivityFrame`,不是测试自己抄一份。建出来的台账是 closed 的,后续
123
+ * `ensureWorkflowActivityLedger` 命中它即早退,离线单测因此永不触网。
124
+ */
125
+ export declare function __feedWorkflowActivityFrameForTests(workflowId: string, frame: WorkflowStreamEvent): void;
66
126
  /**
67
127
  * createLiveWorkflowSource({ baseUrl, authToken, principal, workflowId? }) — the LIVE workflow-monitor source.
68
128
  *