@sema-agent/client-core 0.75.0 → 0.76.0

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -2
  3. package/dist/adapt/arms.js +120 -6
  4. package/dist/adapt/panelTasks.d.ts +6 -2
  5. package/dist/adapt/panelTasks.js +6 -3
  6. package/dist/adapter/downstream/eventToSdkMessage.d.ts +45 -0
  7. package/dist/adapter/downstream/eventToSdkMessage.js +121 -6
  8. package/dist/agentSession/backgroundView.js +2 -1
  9. package/dist/agentsWireCaps.d.ts +15 -2
  10. package/dist/engineAgentPanelStore.d.ts +6 -0
  11. package/dist/engineAgentPanelStore.js +113 -10
  12. package/dist/engineErrorCodes.d.ts +4 -0
  13. package/dist/engineErrorCodes.js +14 -0
  14. package/dist/fleet/fleetLedger.js +6 -0
  15. package/dist/fleet/fleetProjection.js +7 -2
  16. package/dist/fleet/fleetRowAgentType.d.ts +9 -0
  17. package/dist/fleet/fleetRowAgentType.js +59 -0
  18. package/dist/fleetAgentPanelProjection.js +24 -3
  19. package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
  20. package/dist/hitl/approvalOutcomeNote.js +35 -8
  21. package/dist/hitl/approvalResolution.d.ts +148 -0
  22. package/dist/hitl/approvalResolution.js +199 -0
  23. package/dist/hitl/approvalsFeed.d.ts +100 -2
  24. package/dist/hitl/approvalsFeed.js +234 -18
  25. package/dist/hitl/livePendingAsk.d.ts +26 -6
  26. package/dist/hitl/livePendingAsk.js +52 -12
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +3 -0
  29. package/dist/memorySpecWire.d.ts +175 -0
  30. package/dist/memorySpecWire.js +320 -0
  31. package/dist/panelRunningHistory.d.ts +21 -0
  32. package/dist/panelRunningHistory.js +25 -0
  33. package/dist/seam.d.ts +64 -5
  34. package/dist/seam.js +10 -1
  35. package/docs/INTEGRATION-CLIENTS.md +112 -9
  36. package/package.json +1 -1
@@ -99,7 +99,8 @@ export function fleetTaskToBackgroundRow(r) {
99
99
  // [C33]③.2 / server [C36]:launch 时刻 epoch ms(revive 行=本 revive 周期 wake 时刻),
100
100
  // ADDITIVE / tolerate-absent —— 排队中的行本就没有,缺席即键缺席(0 不是「1970 年启动」)。
101
101
  // 两腿同一个 mapper:`snapshot` 首帧行与 `task` 增量帧行(POST-MERGE FULL row)都过这里。
102
- const startedAtMs = typeof r.startedAt === 'number' && r.startedAt > 0 ? r.startedAt : undefined;
102
+ // 与 fleet 投影同律:只有**有限正数**算 launch 锚(非有限值渲不出时长,只会变成 `null`)。
103
+ const startedAtMs = typeof r.startedAt === 'number' && Number.isFinite(r.startedAt) && r.startedAt > 0 ? r.startedAt : undefined;
103
104
  return {
104
105
  key: { source: 'fleet', rowId: r.id },
105
106
  status: backgroundStatusFromFleet(r.status),
@@ -18,10 +18,23 @@ export type TaskAgentWireSkill = {
18
18
  };
19
19
  /** CC 形自定义 agent 的 `memory:` 三词(cli `loadAgentsDir` 的 zod 枚举同源)。 */
20
20
  export declare const AGENT_MEMORY_WORDS: ReadonlySet<string>;
21
- /** `agents[].memory` 上 wire 的形(server `spec-fields.ts` `memoryOk` 闭合白名单的子集:只用 `scopes` + `writeScope`)。 */
21
+ /**
22
+ * `agents[].memory` 上 wire 的形(上游闭合白名单 `{ scopes?, writeScope?, enabled?, scopeContract? }` 的子集:本包只用前两键)。
23
+ *
24
+ * 🔴 **CC-96 放宽**:`writeScope` 此前写死成 `string`(恒在、且不可为 `null`),而 wire 的取值域是 `string | null`
25
+ * (上游直讯确认该域不变)。写死的后果是**读得出却铸不出** —— {@link import('./memorySpecWire.js').readAgentMemorySpec}
26
+ * 能把显式 `null` 读成「这一次 run 对记忆面只读」(无 remember 工具、不做 consolidation 写,recall 仍可),
27
+ * 而端**没有任何办法经本包声明**这一形:只剩「写到某一层」一条路。半条腿的读面比没有更坏,因为它让
28
+ * 「本端支持只读记忆」看起来是真的。
29
+ * · `writeScope: null` = 显式声明只读;
30
+ * · `writeScope` 键缺席 = 未指定(缺省归引擎);
31
+ * · `writeScope: '<层名>'` = 写到那一层。
32
+ * 🔴 放宽的是**类型**,不是本包投影器的纪律:{@link projectAgentsForWire} 仍**恒显式钉** `writeScope`
33
+ * (见下方投影处的 🔴 注),漏钉会让新条目静默写进全局层 —— 那条约束一个字都没松。
34
+ */
22
35
  export type TaskAgentWireMemory = {
23
36
  scopes: string[];
24
- writeScope: string;
37
+ writeScope?: string | null;
25
38
  };
26
39
  /** 发往 server 的单个 agent 定义(白名单内字段的壳侧子集——observer 面壳无语义,恒不发;`memory` 自 0.72.3 按定义发)。 */
27
40
  export type TaskAgentWireItem = {
@@ -37,6 +37,12 @@ export type EngineAgentPanelEvent = {
37
37
  currentAction?: string;
38
38
  toolUses: number;
39
39
  totalTokens: number;
40
+ /**
41
+ * 0.75.1 —— 派生这只子代时**被调用方声明的名字**(wire `task_progress.spawnName`,engine 侧新位)。
42
+ * 🔴 与 `name` 是两位,别合流:`name` 是**展示名**(上游由委派卡的 description 派生),本位是 spawn 时的名字。
43
+ * 🔴 缺席 = **旧形**(产生者还没有这一位)⇒ 键不落,绝不拿展示名冒充。
44
+ */
45
+ spawnName?: string;
40
46
  /**
41
47
  * 0.74.3 CC-70:tick 上带的委派卡 id(wire `task_progress.parentToolCallId`,在场才带)—— 身份归一的**退路键**
42
48
  * (`transcriptId` 查不到时按它反查 fleet 尾段)。
@@ -22,7 +22,7 @@
22
22
  * Buffering: ticks published before the panel mounts are buffered (bounded) and replayed to the first
23
23
  * subscriber, so early frames in a fast turn aren't lost to mount timing.
24
24
  */
25
- import { retirePanelRowRunning, notePanelRowEnded, panelRowEndedIdentity, peekHeldNewbornTerminalRow, takeHeldNewbornTerminalRow, dropHeldNewbornTerminalRow } from './panelRunningHistory.js';
25
+ import { retirePanelRowRunning, notePanelRowEnded, panelRowEndedIdentity, peekHeldNewbornTerminalRow, takeHeldNewbornTerminalRow, dropHeldNewbornTerminalRow, heldNewbornTerminalRefs } from './panelRunningHistory.js';
26
26
  const _panelToolUsesLanePins = [true, true];
27
27
  void _panelToolUsesLanePins;
28
28
  /**
@@ -350,21 +350,73 @@ export function __resetEngineAgentPanelIdentityForTests() {
350
350
  * 0.75.0 W2:一条 end 递达之前,先把投影缓着的「首见即终态」行放出来(它带这一行唯一的一份最终用量),经漏斗原路发布(身份归一照走);
351
351
  * 递达之后记「本进程结过这一行」—— 投影据此把之后到的终态行当成短命子代的收尾(照发用量)而不是历史行。两步都只对当代 end(陈旧判据与销 running 历史同一只)。
352
352
  */
353
- function releaseHeldNewbornTerminal(taskId, endCycle) {
354
- // 先 peek 比周期再决定(异源复审 R3 [high]:先 take 会让前代 end 把当代唯一的缓行取走丢掉):
355
- // end 可证比缓行**旧** ⇒ 缓行留着(等当代的 end);end 可证比缓行**新** ⇒ 缓行作废(前代用量不盖当代行);同代 / 判不出 ⇒ 放出。
353
+ /**
354
+ * 这一条缓行**放出去会落在哪把消费端键上**(纯读口,不动表;与 fleet-row 臂的改写次序同序:
355
+ * 反向所有权 → 已发布的 wire 键 → 自己的行尾段)。第四条腿据它核「行与 end 是否同键」。
356
+ * 🔴 保守方向:所有权在表里就按它答(`wireOwnerOf` 遇到新一代会把它删掉再落到下一档 —— 那一档答错只会让本腿
357
+ * **少放**一次,不会让一只子代分成两行)。
358
+ */
359
+ function heldRowConsumerKey(tail, held) {
360
+ const owner = wireIdByFleetId.get(tail);
361
+ if (owner !== undefined)
362
+ return owner.wireId;
363
+ return publishedWireFor(held) ?? tail;
364
+ }
365
+ /**
366
+ * 这一条 end 对那一行缓着的终态行的**代际三向**(纯读口,不动表)。先 peek 再决定:先 take 会让前代
367
+ * end 把当代唯一的缓行取走丢掉;
368
+ * · `'keep'` = end 可证比缓行**旧** ⇒ 缓行留着(等当代的那条 end);
369
+ * · `'drop'` = end 可证比缓行**新** ⇒ 缓行作废(前代的用量不许盖当代行);
370
+ * · `'release'` = 同代 / 判不出 ⇒ 可以放出;
371
+ * · `'none'` = 这一行没有缓着的终态行。
372
+ */
373
+ function heldNewbornTerminalVerdict(taskId, endCycle) {
356
374
  const held = peekHeldNewbornTerminalRow(taskId);
357
375
  if (held === undefined || held.kind !== 'fleet-row')
358
- return;
376
+ return 'none';
359
377
  const heldCycle = { ...(held.cycleSeq !== undefined ? { cycleSeq: held.cycleSeq } : {}), ...(held.startedAt !== undefined ? { startedAt: held.startedAt } : {}) };
360
378
  if (isStaleEngineAgentPanelEnd(endCycle, heldCycle))
361
- return;
362
- if (isStaleEngineAgentPanelEnd(heldCycle, endCycle)) {
379
+ return 'keep';
380
+ if (isStaleEngineAgentPanelEnd(heldCycle, endCycle))
381
+ return 'drop';
382
+ return 'release';
383
+ }
384
+ /**
385
+ * 🔴 放行前的**同键核**:这一条缓行放出去,行帧与随后那条 end 会不会落在**同一把消费端键**上?
386
+ *
387
+ * 反例(实测):一只子代的委派卡被复用 —— `tick{V, 卡 P}` 已按 V 上过屏;缓着的终态行是
388
+ * `{尾段 X, 转录锚 U, 卡 P}`;来的是 `end{U}`。第四条腿按转录锚找到唯一候选 X,可它一放行就被
389
+ * 「先发布的键为准」经卡 P 改写到 **V** 上,而这条 `end{U}` 在主路上谁也不命中、原样落在 **U**:
390
+ * 用量记到了 V,X 的终态却发给了 U,消费端还可能凭 `end{U}` 另开一行。
391
+ *
392
+ * 所以只在两种形下放行(其余留着缓行,等真正属于它的那条 end —— 宁可晚交一份用量,不让一只子代分成两行):
393
+ * ① 行会被改写到**这条 end 自己那把键**上(随后主路也落在同一把键,`ownedFleetIds` 连带结账);
394
+ * ② 行按**自己的尾段**上屏,而这条 end 的键正是它的转录锚且那把键没被发布过 —— 放行登记了转录钥匙,
395
+ * 主路随即经钥匙表归一到同一条尾段。
396
+ */
397
+ function heldRowReleasesOntoSameKey(tail, end) {
398
+ const held = peekHeldNewbornTerminalRow(tail);
399
+ if (held === undefined || held.kind !== 'fleet-row')
400
+ return false;
401
+ const rowKey = heldRowConsumerKey(tail, held);
402
+ if (rowKey === end.taskId)
403
+ return true;
404
+ // 末一项 `!publishedWireIds.has(...)` 是**可证冗余**的写方守卫(删掉行为不变:若这把键发布过,
405
+ // 上面的 `heldRowConsumerKey` 就会把 rowKey 算成它、第一档已经返回 true,走不到这里)。留着是把
406
+ // 「② 只成立于那把键没上过屏」这句话钉在代码里,别当成活判据去删第一档。
407
+ return rowKey === tail && held.transcriptId === end.taskId && !publishedWireIds.has(end.taskId);
408
+ }
409
+ function releaseHeldNewbornTerminal(taskId, endCycle) {
410
+ const verdict = heldNewbornTerminalVerdict(taskId, endCycle);
411
+ if (verdict === 'drop') {
363
412
  dropHeldNewbornTerminalRow(taskId);
364
413
  return;
365
414
  }
366
- takeHeldNewbornTerminalRow(taskId);
367
- publishEngineAgentPanelEvent(held);
415
+ if (verdict !== 'release')
416
+ return;
417
+ const held = takeHeldNewbornTerminalRow(taskId);
418
+ if (held !== undefined)
419
+ publishEngineAgentPanelEvent(held);
368
420
  }
369
421
  /** 结算(end 已递达):running 历史销、ended 账单调写(可证比已结周期旧的 end 不倒写)。 */
370
422
  function settleRowHistory(taskId, cycle) {
@@ -457,7 +509,10 @@ export function publishEngineAgentPanelEvent(ev) {
457
509
  }
458
510
  case 'end': {
459
511
  // 0.75.0 W2:缓着的「首见即终态」行**先于一切归一**放出(异源复审 R3 [high]:放出的行帧会登记钥匙 / 所有权,之后的 end 归一必须看到它,
460
- // 否则同一子代长成 UUID 行 + 尾段行两行)。三把可能的键:尾段本身 / UUID 经钥匙解析到的尾段 / 这把 UUID 名下的尾段。
512
+ // 否则同一子代长成 UUID 行 + 尾段行两行)。
513
+ // 🔴 0.75.1 —— 前三把键的**真实射程**(实测反例):尾段本身 / UUID 经钥匙解析到的尾段 / 这把 UUID 名下的反向所有权尾段,
514
+ // 后两把**只在行帧真发布过时才登记**,而缓行的定义就是「还没发布」⇒ `tick{UUID}` 先上屏、终态行 `{尾段, transcriptId: UUID}`
515
+ // 被缓住时,这三把全查不到,缓行永远放不出。所以有第四条腿:按缓行**自带的** wire 侧键(transcriptId / 退路键)反查。
461
516
  releaseHeldNewbornTerminal(ev.taskId, ev);
462
517
  {
463
518
  const pre = resolveEnginePanelTaskId(ev.taskId);
@@ -470,6 +525,54 @@ export function publishEngineAgentPanelEvent(ev) {
470
525
  for (const [fleetId, o] of wireIdByFleetId)
471
526
  if (o.wireId === ev.taskId && !o.retired)
472
527
  releaseHeldNewbornTerminal(fleetId, ev);
528
+ // 第四条腿:缓着的行自带 `transcriptId`(主键)与委派卡 id(退路键)—— 两把与 `publishedWireFor` 的**同一条律**:
529
+ // 转录键命中不看退役(同一子代的迟到终态);退路键只认**未退役**的那把 wire 键(复活沿用同一张卡)。
530
+ // 放行仍走 `releaseHeldNewbornTerminal`(代际三向 peek + 经原漏斗归一),所以前代的缓行会被当代终态作废而不是盖上去。
531
+ const heldMatches = [];
532
+ for (const ref of heldNewbornTerminalRefs()) {
533
+ if (ref.taskId === ev.taskId)
534
+ continue; // 第一条腿已经试过这把键
535
+ const byTranscript = ref.transcriptId !== undefined && ref.transcriptId === ev.taskId;
536
+ const byParent = ref.parentToolCallId !== undefined &&
537
+ publishedWireByParent.get(ref.parentToolCallId) === ev.taskId &&
538
+ publishedWireIds.get(ev.taskId)?.retired === false;
539
+ if (byTranscript || byParent)
540
+ heldMatches.push(ref.taskId);
541
+ }
542
+ // 命中之后**先逐条按代际三向判、不动表**,再决定放谁:
543
+ // · 可证更旧的缓行(前代那一行)⇒ 当场作废(它的用量不许盖当代);
544
+ // · end 可证更旧 ⇒ 那条缓行留着,等属于它自己的那条 end;
545
+ // · 剩下的「可放行」候选**恰一条** ⇒ 放行 —— 它一上屏就登记了自己的钥匙 / 所有权,本条 end 随后在主路上
546
+ // 归一到同一把键,所以仍是「一行一条 end」。
547
+ // · 🔴 候选 **≥2**(两个行尾段自称同一个转录锚 / 同一张委派卡)= **身份两义 ⇒ 一条都不放**:转录钥匙表是
548
+ // **单值**的(后写覆盖先写),放两条就会各自上屏成两行而主路只归一到最后那一条 ⇒ 前面那些行永远收不到结算;
549
+ // 换成「每行各自补一条 end」则要在这里替主路决定消费端键,会吞掉原 wire 键那一行自己的终态。身份对不上就
550
+ // **一帧都不摘**是本包扣留表的既有律(见 `hitl/gateLedger.ts` 的「判不出 ⇒ 一条都不摘」)。代价 = 这种形下
551
+ // 那几条行的用量仍缺席(与本件修之前同),换来的是绝不凭空多长一行、绝不替谁编一条终态。
552
+ // 🔴 **理由更正(0.76.0;外部验收方 [7764] 逐条回一级源码亲核后证伪了本句旧版)**:旧注释写的是「两个行尾段共享一个
553
+ // **转录锚**不成立(转录锚按代际唯一),所以这一支是防御面」—— 那句话**只论证了转录钥匙那条腿**,而 `heldMatches`
554
+ // 是**转录锚腿 ∪ 委派卡腿**的并集,两条腿异质:
555
+ // · 委派卡那条腿的前提**为假** —— workflow 扇出下一张卡整跑冻结成常量、尾段逐 callKey 不同,**最多 50 个**
556
+ // 尾段共享同一张卡且可同时在飞(上游坐标:core `workflow.ts` 的 `bceParentToolCall` 与 `waIdOf(callKey)`;
557
+ // 默认 `maxAgents` 50)。这正是本包 lane 判据里 `workflow-card-parent` 那一支的上游本体。
558
+ // 🔴 上游当事方 09-20 直讯确认了这一形:**一次扇出的全部子代共享同一张 `parentToolCallId`**(那次 workflow
559
+ // 工具调用自己的卡),逐子代能区分的键**只有 `taskId`**(`wa*` 形)—— 也就是说委派卡这条腿在扇出下**天然多对一**。
560
+ // · 更要紧:**凑够 ≥2 根本不需要两行共享任何一个键** —— 一行只经转录锚命中、另一行只经委派卡命中即可。
561
+ // ⇒ 今天这一支之所以罕见,**靠的不是上面那条律**,而是「workflow 子代的 tick 被 lane 门拦掉 ⇒ 委派卡那条腿的
562
+ // `publishedWireByParent` 没有写入」这条**更脆**的保证 —— 它历史上塌过(上游投影白名单曾把 `workflowRunId` 整个剥掉)。
563
+ // **失效条件**:workflow 子代的 tick 一旦上屏(白名单变宽 / lane 门放行),这一支立刻变成常态路径。
564
+ // 所以本分支**不是**「理论上才会走到的防御面」,它是**有真上游机制**的一支,只是今天被一条脆保证挡着。
565
+ // 🔴 无论放没放,**主路照走**:原 wire 键那一行(它可能早就上过屏)自己的 end 绝不能被这条腿吞掉。
566
+ const releasable = [];
567
+ for (const tail of heldMatches) {
568
+ const verdict = heldNewbornTerminalVerdict(tail, ev);
569
+ if (verdict === 'drop')
570
+ dropHeldNewbornTerminalRow(tail);
571
+ else if (verdict === 'release' && heldRowReleasesOntoSameKey(tail, ev))
572
+ releasable.push(tail);
573
+ }
574
+ if (releasable.length === 1)
575
+ releaseHeldNewbornTerminal(releasable[0], ev);
473
576
  }
474
577
  // 先发布的键为准(CC-83):尾段已被改写成 UUID 的子代,fleet 侧(投影 / 通知)的 end{尾段} 也改写成 UUID
475
578
  const wireOwner = wireOwnerOf(ev.taskId, ev);
@@ -66,6 +66,10 @@ export declare const CONFIG_LIMIT_UNKNOWN_KEY = "config.limit_unknown_key";
66
66
  * 🔴 与本族其余成员同律:**坏旋钮响亮拒,绝不静默折回缺省**。
67
67
  */
68
68
  export declare const CONFIG_DELEGATION_ENTRY_CAPS = "config.delegation_entry_caps";
69
+ /** 记忆面 project key 的拼写不合法(**400**;构造期拒,什么都没跑)。 */
70
+ export declare const CONFIG_MEMORY_PROJECT_KEY_SPELLING = "config.memory_project_key_spelling";
71
+ /** 这次请求的记忆写入范围与已生效的那一份不一致(**409**;构造期拒,什么都没跑 —— 冲突,不是拼写错)。 */
72
+ export declare const CONFIG_MEMORY_WRITE_SCOPE_MISMATCH = "config.memory_write_scope_mismatch";
69
73
  /**
70
74
  * 已知的配置拒绝码(**识别表,非白名单**)。判「这是不是一条配置拒绝」请用
71
75
  * {@link isConfigRefusalCode} —— 它按 `config.` 前缀判,未来新成员自动落进来。
@@ -76,6 +76,18 @@ export const CONFIG_LIMIT_UNKNOWN_KEY = 'config.limit_unknown_key';
76
76
  * 🔴 与本族其余成员同律:**坏旋钮响亮拒,绝不静默折回缺省**。
77
77
  */
78
78
  export const CONFIG_DELEGATION_ENTRY_CAPS = 'config.delegation_entry_caps';
79
+ // ── 记忆面的两个**构造期**拒码(CC-96;`agents[].memory` / 请求记忆面的同族第二波)────────────
80
+ //
81
+ // 两码与本族其余成员同律(「配置错了」不是「跑失败了」),但**多一条**:它们拒在**受理之前** ——
82
+ // 这一次运行什么都没跑,所以出路一律是「改请求再发」,不是等、也不是去查运行结果。
83
+ // 🔴 **分诊归引擎,本包不预铸第二判官**:拼写合不合法 / 范围算不算不匹配,判据都在引擎侧;本端照码渲一句话
84
+ // (单铸口 = `src/memorySpecWire.ts` 的 `memoryConfigRefusalNoteOf`),绝不自己先算一遍再「帮」它判。
85
+ // ⚠️ 两码的 HTTP 状态**不同**(本族此前清一色 400):拼写 400 / 不匹配 409。状态不是判据(判据是码),
86
+ // 但它是**处置差**的来源 —— 一条是打字错,一条是与已生效的那一份冲突,渲成同一句会把两种出路混掉。
87
+ /** 记忆面 project key 的拼写不合法(**400**;构造期拒,什么都没跑)。 */
88
+ export const CONFIG_MEMORY_PROJECT_KEY_SPELLING = 'config.memory_project_key_spelling';
89
+ /** 这次请求的记忆写入范围与已生效的那一份不一致(**409**;构造期拒,什么都没跑 —— 冲突,不是拼写错)。 */
90
+ export const CONFIG_MEMORY_WRITE_SCOPE_MISMATCH = 'config.memory_write_scope_mismatch';
79
91
  /**
80
92
  * 已知的配置拒绝码(**识别表,非白名单**)。判「这是不是一条配置拒绝」请用
81
93
  * {@link isConfigRefusalCode} —— 它按 `config.` 前缀判,未来新成员自动落进来。
@@ -86,6 +98,8 @@ export const CONFIG_REFUSAL_CODES = new Set([
86
98
  CONFIG_STRATEGY_MAX_SIZE_INVALID,
87
99
  CONFIG_LIMIT_UNKNOWN_KEY,
88
100
  CONFIG_DELEGATION_ENTRY_CAPS,
101
+ CONFIG_MEMORY_PROJECT_KEY_SPELLING,
102
+ CONFIG_MEMORY_WRITE_SCOPE_MISMATCH,
89
103
  ]);
90
104
  // ── 委派席位到限族(core 5.48.0 design/323,[4743] @cli 点名的「两新 coded 拒绝」)──────────────
91
105
  //
@@ -7,6 +7,7 @@ import { enqueueBgChildNotification, isOwnWorkflowRun } from '../notifications.j
7
7
  import { clearBgTerminalFacts, getBgTerminalFacts, isOwnEngineRun, recordBgParentRun, recordBgTerminalFacts, recordOwnEngineRun, } from '../subagentContentStore.js';
8
8
  import { rowIdTail } from '../workflow.js';
9
9
  import { recordEngineTranscriptId } from '../subagent/engineDelegatedPrompt.js';
10
+ import { recordFleetRowAgentType } from './fleetRowAgentType.js';
10
11
  import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
11
12
  import { TERMINAL_FLEET_TASK_STATUSES, projectTasks, projectWorkflows, wireCycleSeq, wireParentId, } from './fleetProjection.js';
12
13
  /** 终态行留存宽限窗(187 的 agents 页把完成会话留在列表;窗过即净,防长会话堆行)。 */
@@ -345,6 +346,11 @@ export function createFleetLedger(hooks = {}, opts = {}) {
345
346
  if (typeof row.transcriptId === 'string' && row.transcriptId.length > 0) {
346
347
  recordEngineTranscriptId(rowIdTail(row.id), row.transcriptId);
347
348
  }
349
+ // 0.75.1:`agentType`(§K-7 短类型 label)在 `projectTasks` 里会被**折进** `name`
350
+ // (`deriveAgentLabel` = agentName ?? agentType),折完就与显示名分不开了 —— 而 `subagent_lifecycle`
351
+ // 的 `agentType` 需要的正是折之前那一位。所以在这里(折之前)按行 id 尾段留一份;缺席 / 坏形不入账
352
+ // (读口答缺席,tick 臂让键缺席,绝不拿展示名冒充)。写方只有这一处,与转录锚台账同族同作用域。
353
+ recordFleetRowAgentType(rowIdTail(row.id), row.agentType);
348
354
  // 终态帧 → 留存池(remove 同毫秒尾随,probe 实证;留存让 completed band 数得到它)。
349
355
  // REF-CC-040(fleet2-02):非终态分支必须显式 delete —— 否则 bg 复活(SendMessage 后
350
356
  // seq+1)时,`project()` 因 `taskMap.has(id)` 恒 continue 而永不清理过期留存条目,belt
@@ -341,7 +341,8 @@ export function projectTasks(allRows, opts) {
341
341
  ...(transcriptId !== undefined ? { transcriptId } : {}),
342
342
  // launch 真锚原值透传(> 0 才算有值)。ledger 拿它算 elapsedMs 是**另一件事** —— 下游要的是
343
343
  // 时刻本身(自己的钟去减),elapsedMs 是发帧那一刻的快照,客户端存着会漂([C36] server 判据)。
344
- ...(typeof r.startedAt === 'number' && r.startedAt > 0 ? { startedAt: r.startedAt } : {}),
344
+ // launch 锚只认**有限正数**;非有限值按缺席(它序列化成 `null` 发出去 = 一个不存在的 launch 时刻)。
345
+ ...(typeof r.startedAt === 'number' && Number.isFinite(r.startedAt) && r.startedAt > 0 ? { startedAt: r.startedAt } : {}),
345
346
  ...(currentTool !== undefined ? { currentTool } : {}),
346
347
  ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
347
348
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
@@ -511,7 +512,11 @@ export const FLEET_TASK_ROW_KEYS_NOT_PROJECTED = {
511
512
  agentType: {
512
513
  kind: 'by-design',
513
514
  owner: 'client-core 规范重构轮',
514
- reason: '折进 `name`(deriveAgentLabel:agentName ?? agentType,都缺才退 objective 派生)——渲染契约只有一个身份列,187 的 tjl 同形。',
515
+ reason: '折进 `name`(deriveAgentLabel:agentName ?? agentType,都缺才退 objective 派生)——渲染契约只有一个身份列,与上游的身份选择器同形。' +
516
+ '🔴 0.75.1 CC-95(b) 补一句射程:**本视图**仍不带它(渲染面一个身份列不变),但它不再只被折掉 —— ' +
517
+ '`fleet/fleetLedger` 的行帧臂在折之前把它记进 `fleet/fleetRowAgentType`(包内叶),adapt 的 tick 臂据此给 ' +
518
+ '`subagent_lifecycle.agentType` 取诚实来源(此前取的是展示名 = 委派卡 description 派生的串)。' +
519
+ '所以这条是「不进本视图」,不是「零消费者」;哪天渲染面要第二个身份列,再把它投影出来并删本条。',
515
520
  },
516
521
  agentName: {
517
522
  kind: 'by-design',
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 记一条 fleet 行的 agentType(幂等;行帧不带这一位 / 坏形 ⇒ 整条不动,不擦掉已知值 ——
3
+ * 同一行的后续帧缺席这一位是「这帧没说」,不是「类型没了」)。
4
+ */
5
+ export declare function recordFleetRowAgentType(taskIdTail: string, agentType: string | undefined): void;
6
+ /** 读口:这一行的子代类型;`undefined` = **诚实不知道**(调用方让键缺席,不许回落到展示名)。 */
7
+ export declare function fleetRowAgentType(taskIdTail: string | undefined): string | undefined;
8
+ /** 测试钩:清台账(module 级单例,同进程多组断言必须能清)。 */
9
+ export declare function __resetFleetRowAgentTypeForTests(): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * fleetRowAgentType — 「这一条 fleet 行的子代是什么**类型**」的台账(0.75.1 CC-95(b);包内叶,不出公面)。
3
+ *
4
+ * ── 为什么要有它 ──────────────────────────────────────────────────────────────────────────────
5
+ * `subagent_lifecycle` 的 `agentType`(CC `SubagentStart.agent_type` 那一位)此前取的是 tick 的**展示名**
6
+ * `task_progress.name` —— 那一位是上游由委派卡的 `description` 派生的**显示串**,不是类型。于是宿主的
7
+ * SubagentStart hook 收到的是任务描述:一个看起来合理的错答案。
8
+ *
9
+ * 诚实来源亲核(sdk 9.8.1 实装字节):
10
+ * · `task_progress` 臂(`dist/events.d.ts`)上**没有** `agentType`、也没有 `spawnAgentType` —— tick 这条
11
+ * lane 今天根本不报类型,所以本层**不读**无出处的键(读了就是替上游发明一个字段)。🔴 **措辞更正(外部验收方 [7764])**:因此 tick 这一档**不是一条「今天恒不命中的路径」,它根本没有代码** —— 本文件里 `agentType` 的三处出现全在注释,唯一的代码走的是 fleet 行帧那一档。相应地,「上游铸位当天零改生效」这句**不成立**:上游若铸出 `spawnAgentType`,本包**没有读口**,仍需改码补一档;
12
+ * · `FleetTaskRow.agentType`(`dist/resources/fleet.d.ts`,§K-7 的短类型 label,如 `Explore` /
13
+ * `general-purpose`)**有出处** —— 但 `fleet/fleetProjection.ts` 把它**折进** `FleetTaskView.name`
14
+ * (`deriveAgentLabel` = `agentName ?? agentType`,登记在
15
+ * `FLEET_TASK_ROW_KEYS_NOT_PROJECTED` 里),折完之后它与显示名再也分不开。
16
+ * ⇒ 所以类型这一位要在**折之前**留一份:行帧消费臂按行 id 尾段记,tick 臂按归一后的尾段读。
17
+ *
18
+ * ── 纪律 ────────────────────────────────────────────────────────────────────────────────────
19
+ * 🔴 **只记 wire 真给了的**:空串 / 非串一律不入账(空串既不是类型也不是「不知道」);账上没有 ⇒ 读口
20
+ * 答 `undefined` = **诚实缺席**,调用方让键缺席,绝不回落到展示名或编一个 `subagent`。
21
+ * 🔴 作用域 = 进程级(与本包转录锚台账逐字同族:写方 = 行帧消费臂,读方 = tick 臂;两份模块实例 ⇒ 读口恒空、
22
+ * `agentType` 恒缺席,而两边代码看着都对)。
23
+ * 有界 LRU(命中即续命):上限是内存约束不是语义,被挤掉只退化成「这一行的类型不知道」。
24
+ */
25
+ const MAX_ROWS = 2048;
26
+ const agentTypeByTail = new Map();
27
+ /**
28
+ * 记一条 fleet 行的 agentType(幂等;行帧不带这一位 / 坏形 ⇒ 整条不动,不擦掉已知值 ——
29
+ * 同一行的后续帧缺席这一位是「这帧没说」,不是「类型没了」)。
30
+ */
31
+ export function recordFleetRowAgentType(taskIdTail, agentType) {
32
+ if (typeof taskIdTail !== 'string' || taskIdTail.length === 0)
33
+ return;
34
+ // 形核仍在本层做(wire 值不可信,型面说是 string 不等于到货一定是 string;空串既不是类型也不是「不知道」)。
35
+ if (typeof agentType !== 'string' || agentType.length === 0)
36
+ return;
37
+ agentTypeByTail.delete(taskIdTail);
38
+ agentTypeByTail.set(taskIdTail, agentType);
39
+ if (agentTypeByTail.size > MAX_ROWS) {
40
+ const oldest = agentTypeByTail.keys().next().value;
41
+ if (oldest !== undefined)
42
+ agentTypeByTail.delete(oldest);
43
+ }
44
+ }
45
+ /** 读口:这一行的子代类型;`undefined` = **诚实不知道**(调用方让键缺席,不许回落到展示名)。 */
46
+ export function fleetRowAgentType(taskIdTail) {
47
+ if (typeof taskIdTail !== 'string' || taskIdTail.length === 0)
48
+ return undefined;
49
+ const v = agentTypeByTail.get(taskIdTail);
50
+ if (v === undefined)
51
+ return undefined;
52
+ agentTypeByTail.delete(taskIdTail); // LRU 续命:还在飞的子代不被一阵新登记挤掉
53
+ agentTypeByTail.set(taskIdTail, v);
54
+ return v;
55
+ }
56
+ /** 测试钩:清台账(module 级单例,同进程多组断言必须能清)。 */
57
+ export function __resetFleetRowAgentTypeForTests() {
58
+ agentTypeByTail.clear();
59
+ }
@@ -48,7 +48,7 @@
48
48
  * 就地维护第二份;该函数出过等价性事故,0.1.1 修,见 `workflow.ts` 头注)。
49
49
  */
50
50
  import { isStaleEngineAgentPanelEnd, publishEngineAgentPanelAbsence, publishEngineAgentPanelEvent } from './engineAgentPanelStore.js';
51
- import { __resetPanelRunningHistoryForTests, notePanelRowRunning, panelRowUnknownToConsumer, panelRowEndedIdentity, holdNewbornTerminalRow, dropHeldNewbornTerminalRow } from './panelRunningHistory.js';
51
+ import { __resetPanelRunningHistoryForTests, notePanelRowRunning, panelRowUnknownToConsumer, panelRowEndedIdentity, holdNewbornTerminalRow, peekHeldNewbornTerminalRow, dropHeldNewbornTerminalRow } from './panelRunningHistory.js';
52
52
  import { rowIdTail } from './workflow.js';
53
53
  import { TERMINAL_FLEET_TASK_STATUSES } from './fleet/fleetProjection.js';
54
54
  import { DEFAULT_SESSION_KEY } from './sessionSlot.js';
@@ -146,7 +146,11 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
146
146
  const toolUses = typeof row.toolUses === 'number' ? row.toolUses : undefined;
147
147
  const transcriptId = row.transcriptId || undefined;
148
148
  const parentToolCallId = typeof row.parentToolCallId === 'string' && row.parentToolCallId.length > 0 ? row.parentToolCallId : undefined;
149
- const startedAt = typeof row.startedAt === 'number' && row.startedAt > 0 ? row.startedAt : undefined;
149
+ // 🔴 形核与周期判据(`isStaleEngineAgentPanelEnd` / `isNewEngineAgentPanelCycle`)**逐字同源**:
150
+ // 有限正数才算数。少一个 `Number.isFinite` 就会让 `Infinity` 进台账 —— 那两只判据按缺席看它,
151
+ // 而本层按在场看它,同一帧在两处得出相反的「可比不可比」。
152
+ // `Infinity` 还会被序列化成 `null` 发给消费端,渲成一个不存在的 launch 时刻。
153
+ const startedAt = typeof row.startedAt === 'number' && Number.isFinite(row.startedAt) && row.startedAt > 0 ? row.startedAt : undefined;
150
154
  // 0.74.0:代际号(正整数才算;坏形 / 缺席 ⇒ undefined,绝不补 1)。
151
155
  const cycleSeq = typeof row.cycleSeq === 'number' && Number.isInteger(row.cycleSeq) && row.cycleSeq >= 1 ? row.cycleSeq : undefined;
152
156
  const currentTool = currentToolKeyOf(row.currentTool) !== undefined ? row.currentTool : undefined;
@@ -271,6 +275,23 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
271
275
  });
272
276
  continue;
273
277
  }
278
+ // 🔴 0.75.1:缓着一条「首见即终态」的行时,后到的 running 帧有**三向**(0.75.0 是无条件作废 ⇒ 同代陈旧快照
279
+ // 把终态那条的最终用量顶掉,面板最后显示的是快照里的小数字):
280
+ // · **可证同代**(两侧有可比的同一种周期键且相等)= 陈旧快照 —— 与终态分支 `settledThisCycle` 同律:
281
+ // 整帧不落地(零事件、台账保持 settled),缓行留着等 end;
282
+ // · **可证更新** ⇒ 真复活:照发 running 行,缓着的前代终态行作废(下面那句 drop);
283
+ // · **判不出**(任一侧没有可比的周期键)⇒ 维持 0.75.0 的行为(当复活、作废缓行)—— 缺席不制造周期,
284
+ // 这一向是**已知并接受**的限制(接入文档的缺口行有记)。
285
+ // 「可证更旧」不在这三向里:它在本循环开头就被 `isStaleEngineAgentPanelEnd(rowIdentity, prevIdentity)` 整只跳过了。
286
+ // 可比性只由**帧自带的**两键与台账里记着的两键决定(不能用 `knownCycleSeq` / `knownStartedAt` —— 那两位会把
287
+ // 缺席沿用成「与台账相等」,把「判不出」误判成「可证同代」)。判据与 `isNewEngineAgentPanelCycle` 的前件同形。
288
+ const cycleComparable = (cycleSeq !== undefined && prev?.cycleSeq !== undefined) || (startedAt !== undefined && prev?.startedAt !== undefined);
289
+ // 🔴 再加一道 `prev.settled` 的窄化:缓行只由**终态**分支铸(铸完台账即 settled),所以「缓着行而台账说它在跑」
290
+ // 在本会话的台账里不可达;写出来是为了让这一格永远只对**结过的行**生效 —— 缓行表是进程级、seenMap 是按
291
+ // sessionKey 分桶的两个作用域,不写这一句的话,别的会话缓的行会让本会话一条真在跑的行被判成陈旧快照。
292
+ const staleSnapshotOverHeldRow = prev !== undefined && prev.settled === true && cycleComparable && !rowNewerCycle && peekHeldNewbornTerminalRow(taskId) !== undefined;
293
+ if (staleSnapshotOverHeldRow)
294
+ continue;
274
295
  // ① 只发变化(新行 / status 翻 / token 涨 / 累计工具数涨 / 转录锚首次到)。
275
296
  // 🔴 后两项的比较是**单向**的:只有「这一帧真给了值且与台账不同」才算变化。帧上**缺席**
276
297
  // 不算变化(缺席=引擎这帧没说,不是「回到不知道」)——否则新旧引擎混发时台账会在
@@ -287,7 +308,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
287
308
  (cycleSeq !== undefined && prev.cycleSeq !== cycleSeq) ||
288
309
  (currentToolKey !== undefined && prev.currentToolKey !== currentToolKey)) {
289
310
  notePanelRowRunning(taskId); // CC-86:真发布了这一行在跑才记历史(未变的陈旧快照零事件也零记账,复审 R3)
290
- dropHeldNewbornTerminalRow(taskId); // 复活为 running ⇒ 缓着的旧终态行作废(用量以新一代为准)
311
+ dropHeldNewbornTerminalRow(taskId); // 复活为 running ⇒ 缓着的旧终态行作废(用量以新一代为准);可证同代的陈旧快照到不了这里(三向判定,见上)
291
312
  publishEngineAgentPanelEvent({
292
313
  kind: 'fleet-row',
293
314
  taskId,
@@ -1,13 +1,3 @@
1
- /**
2
- * approvalOutcomeNote — 流内帧腿的审批结局回交(`AskGateWireDeps.onToolApprovalOutcome(frame, outcome)`,0.74.1 CC-68)
3
- * 到宿主「审批结局便签」的**单源映射**(0.74.5 CC-88;此前三端装配点各手拼一份,语义以 cli 1.0.123 现行为准):
4
- * · `approvalId` = 帧上的原词;
5
- * · `settled` = `outcome.decision !== 'unresolved'`(respond 送达并落定;**不看** ack 在不在 —— ack 是 server 的回执形,宿主不读);
6
- * · `retracted` 只在 `outcome.retracted === true` 时带键(**独立判别位**,不折进 detail:撤卡 ≠ 投递失败 —— 释放认领但不扣重出卡预算);
7
- * `editRefused` **不算** retracted,只折进 detail;
8
- * · `detail` 三支优先序:撤卡固定句 → 编辑被拒固定句 → `respondRefusal` 的 errorCode / message 过滤非空后 ` · ` 拼接;
9
- * 拼出空串 ⇒ 键缺席(绝不编一句;不用决断词、不用 ack 摘要)。
10
- */
11
1
  import type { ToolApprovalFrame, ToolApprovalFrameOutcome } from './toolApprovalWire.js';
12
2
  /** 宿主便签形(三端同一形;`retracted` / `detail` 在场才带)。 */
13
3
  export interface StreamApprovalOutcomeNote {
@@ -1,20 +1,47 @@
1
+ /**
2
+ * approvalOutcomeNote — 流内帧腿的审批结局回交(`AskGateWireDeps.onToolApprovalOutcome(frame, outcome)`,0.74.1 CC-68)
3
+ * 到宿主「审批结局便签」的**单源映射**(0.74.5 CC-88;此前三端装配点各手拼一份,语义以 cli 1.0.123 现行为准):
4
+ * · `approvalId` = 帧上的原词;
5
+ * · `settled` = 这次决断**落定了**(respond 送达并落定;**不看** ack 在不在 —— ack 是 server 的回执形,宿主不读);
6
+ * 判据 = {@link approvalResolutionOf} 的落定臂,与旧口径 `outcome.decision !== 'unresolved'` 在实装可达的每一形上同值
7
+ * (撤卡 / 编辑被拒两形只与 `'unresolved'` 同场铸出);
8
+ * · `retracted` 只在 `outcome.retracted === true` 时带键(**独立判别位**,不折进 detail:撤卡 ≠ 投递失败 —— 释放认领但不扣重出卡预算);
9
+ * `editRefused` **不算** retracted,只折进 detail;
10
+ * · `detail` 三支优先序:撤卡固定句 → 编辑被拒固定句 → `respondRefusal` 的 errorCode / message 过滤非空后 ` · ` 拼接;
11
+ * 拼出空串 ⇒ 键缺席(绝不编一句;不用决断词、不用 ack 摘要)。
12
+ *
13
+ * 🔴 **三位的判据自本版起由 {@link approvalResolutionOf} 派生**(「一次决断最后怎么了」的单源判别联合):
14
+ * `settled` = 落定臂;`retracted` = 「零字节 · 撤卡」这一 cause;`detail` 的三支 = 那三个臂/cause 的
15
+ * 三个分支。本便签的**导出形与逐字输出不变** —— 变的只是判据不再在这里第二次手读旁挂布尔位
16
+ * (同一个问题两处各判一次 = 两份判据必然分叉)。
17
+ */
18
+ import { approvalNotSentCause, approvalResolutionOf, isApprovalDecided, } from './approvalResolution.js';
1
19
  export const APPROVAL_NOTE_RETRACTED_DETAIL = 'the card was retracted (this host no longer has a decision port for it)';
2
20
  export const APPROVAL_NOTE_EDIT_REFUSED_DETAIL = 'edits are not accepted on this card';
3
21
  function nonEmpty(v) {
4
22
  return typeof v === 'string' && v.length > 0 ? v : undefined;
5
23
  }
24
+ /** 结局的三支 detail(与 {@link ApprovalResolution} 的臂/cause 一一对应;拼出空串 = 没有可交的原文)。 */
25
+ function detailOf(r) {
26
+ const cause = approvalNotSentCause(r);
27
+ if (cause === 'retracted')
28
+ return APPROVAL_NOTE_RETRACTED_DETAIL;
29
+ if (cause === 'edit_refused')
30
+ return APPROVAL_NOTE_EDIT_REFUSED_DETAIL;
31
+ // 引擎那一头给的原文:机器码 + 拒句,各自非空才进,` · ` 拼(不复述决断词、不摘 ack)。
32
+ if (r.kind === 'unsettled') {
33
+ return [nonEmpty(r.errorCode), nonEmpty(r.detail)].filter((x) => x !== undefined).join(' · ');
34
+ }
35
+ return '';
36
+ }
6
37
  /** 帧 + 结局 → 便签(纯函数;结局上的坏形按缺席读,不抛)。 */
7
38
  export function approvalOutcomeNoteOf(frame, outcome) {
8
- const retracted = outcome.retracted === true;
9
- const refusal = outcome.respondRefusal;
10
- const detail = retracted
11
- ? APPROVAL_NOTE_RETRACTED_DETAIL
12
- : outcome.editRefused === true
13
- ? APPROVAL_NOTE_EDIT_REFUSED_DETAIL
14
- : [nonEmpty(refusal?.errorCode), nonEmpty(refusal?.message)].filter((x) => x !== undefined).join(' · ');
39
+ const r = approvalResolutionOf(outcome);
40
+ const retracted = approvalNotSentCause(r) === 'retracted';
41
+ const detail = detailOf(r);
15
42
  return {
16
43
  approvalId: frame.approvalId,
17
- settled: outcome.decision !== 'unresolved',
44
+ settled: isApprovalDecided(r),
18
45
  ...(retracted ? { retracted: true } : {}),
19
46
  ...(detail.length > 0 ? { detail } : {}),
20
47
  };