@sema-agent/client-core 0.11.18 → 0.11.19

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
@@ -23,7 +23,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
23
23
 
24
24
  ## Scope
25
25
 
26
- **Version:** 0.11.18
26
+ **Version:** 0.11.19
27
27
 
28
28
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
29
29
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -22,16 +22,65 @@
22
22
  * ── 'not-configured' 判别(诚实缺席纪律)────────────────────────────────────────────────────
23
23
  * · fleet:面 gated off 时路由在任何帧之前答 **501**(typed)—— 501 ⇒ 'not-configured',
24
24
  * 其它错 ⇒ 'unavailable'(拒绝形分档;错误判型走 controlRouter.errCodes 单源)。
25
- * · assistant:无 TiDB run store 时 worker 回 `{tasks:[]}` **原样透传** —— wire 上与真空
26
- * 不可区分。能探则探:`GET /v1/capabilities` 的 `scheduler` 位(server `schedulerEnabled &&
27
- * requirePrincipal!==true && remoteExec==="host"` 三与)为 false/缺席 ⇒ 'not-configured';
28
- * caps 面自己不可达 ⇒ 判别缺席,**如实回 'ok'**(绝不编造判别)。🔴 残余缺口候引擎位:
29
- * scheduler:true 而 run store 未配的 `{tasks:[]}` 仍与真空同形 —— 需要 wire 长出判别位
30
- * (caps 或响应信封)才能收口,本实现不猜。
25
+ * · assistant:无 run store 时 worker 回 `{tasks:[]}` **原样透传** —— wire 上与真空不可区分。
26
+ * 能探则探:`GET /v1/capabilities` 的 **`approvals`** 位为 false ⇒ 'not-configured';
27
+ * caps 面自己不可达 判别缺席,**如实回 'ok'**(绝不编造判别)。
31
28
  *
32
- * 字段纪律:缺席=键不在场(undefined),🔴 绝不 `?? 0` / `?? false`;ISO→epoch ms 只在
33
- * assistant 行边界转一次;fleet 复合 id("<runId> <taskId>")整串不拆;fleet 子行(parentId
34
- * 在场)不进 rows(子代不是用户的独立后台任务)。
29
+ * 🔴 3 考据(读错键的定谳,小黑板 [C33]③.3 server [C34],server 3.13.0 已把辖域写进
30
+ * 代码注与 ARCHITECTURE):本实现此前读 `scheduler`,那是**错的键**。
31
+ * · `/v1/assistant/*` 的挂载条件是 `checkpointStore`(server `routes/approvals-assistant.ts:67`
32
+ * 的分支头:`if (deps.checkpointStore && (url.startsWith("/v1/approvals") ||
33
+ * url.startsWith("/v1/assistant")))`),而 caps 里**同一个谓词**的键是 `approvals`
34
+ * (`capabilities.ts:56` `approvals: Boolean(deps.checkpointStore)`)。键名窄(approvals)、
35
+ * 辖域宽(整条 assistant 车道)——按名字猜会猜错,这是上游文档欠账不是消费端不小心。
36
+ * · `scheduler` 是**另一回事**:host 单用户腿的自唤醒 cron 工具(CronCreate/CronDelete/
37
+ * CronList),它对自己承诺的东西是诚实的,与「计划运行视图在不在」无关。
38
+ * · 实测后果(3.7.0 local 形,DB_BACKEND=local 无 DURABLE_APPROVAL):caps.scheduler=true 而
39
+ * `GET /v1/assistant/tasks`=404 ⇒ 面板**永久**渲「Scheduled runs can't be reached right now」。
40
+ * · 🔴 两个看似更省事的修法都**不采**(server [C34] 逐条否掉):(a) 把 404 并进
41
+ * not-configured —— 会吞掉「caps 说有路由却 404」这个真信号(真的漏注册/误删就再也看不见);
42
+ * (b) server 把 checkpointStore 与进 scheduler —— 会污染一个本来准确的键。
43
+ * **判别规则一个字没改,只改读哪个键。**
44
+ * · 🔴 换键**不**收口的残余缺口(照旧候引擎位,本实现不猜):`approvals:true`(checkpointStore
45
+ * 在)而 run store 未配时,worker 仍回 `{tasks:[]}` —— 那份空列表与「真的没有任务」在
46
+ * wire 上依旧同形。要收口需要 wire 长出判别位(caps 或响应信封),不是客户端能推出来的。
47
+ *
48
+ * 字段纪律:缺席=键不在场(undefined),🔴 绝不 `?? 0` / `?? false` / `?? ''`;ISO→epoch ms 只在
49
+ * assistant 行边界转一次(updatedAt/createdAt 同一个转换器,fleet 的 startedAt 本就是 epoch ms
50
+ * 直出);fleet 复合 id("<runId> <taskId>")整串不拆;fleet 子行(parentId 在场)不进 rows
51
+ * (子代不是用户的独立后台任务)。两源的时间锚是**两个不同的量**,各占各的契约位、互不顶替:
52
+ * assistant 给 `createdAtMs`(创建时刻)、fleet 给 `startedAtMs`(launch 时刻)——见 contract.ts
53
+ * 两处逐位说明。fleet 行的 `sessionId`/`scope` 被 server `stripT` 剥掉不上 wire,别在投影里等。
54
+ *
55
+ * ── 📋 下一批工单(件 4 调研,不在本批实现):BackgroundRow 的 canonical stop 能力判别 ────────
56
+ * 起因(小黑板 [C33]③.4):web 报「`runs.cancel` 的 id 空间是 taskId,而 fleet 顶层行 id 是 runId、
57
+ * 子行是复合 id」⇒ 他们对 fleet 行**不渲 Stop**(没有已挣得的取消路径就不摆按钮 —— 这个处置本身
58
+ * 是对的,server [C34] 也同意)。主会话裁定:**不是「fleet 行没有停止路径」,而是要按行类型分**。
59
+ * 下一批要落的形状(本批只调研落档,不新建 API):
60
+ * ① **能力判别**应当是行上的一等位(建议 `stop?: { verb, target }` 或等价判别),由包算、端只读
61
+ * —— 端各自判 = 每端一份猜测,正是本批修的那类病。缺席即「本行不可停」,端据此**显隐** Stop。
62
+ * ② **verb × id 映射**(包内既有件,全部已核实):
63
+ * · agent 行(fleet 顶层/子行):`runs.taskStop(hostRunId, handle)`,包内封装 =
64
+ * `src/subagent/engineTaskHandleWire.ts`(`POST /v1/runs/:id/tasks/:handle/stop?session=`)。
65
+ * **handle 取法**:复合 id `"<runId> <taskId>"` 取尾段 = 引擎 taskId —— 包内已有同语义实现
66
+ * `rowIdTail()`(`src/workflow.ts:24`,导出;`src/fleetAgentPanelProjection.ts:94` 有零依赖叶
67
+ * 的私有同款副本);宿主 run 解析走 `getBgParentRun(handle) ?? activeEngineRunId()`。
68
+ * **能力门** = `capabilities.taskHandles`(`src/subagent/engineRowStopGate.ts`
69
+ * `engineTaskHandlesCapable()`),🔴 别 trial-by-404。
70
+ * **回执纪律**(同文件 `engineRowNeedsStopConfirm()`):只有 **200 算停了**;409
71
+ * `stop.not_landed` / `stop.not_local` 一律保持原状(引擎里可能还在跑),端绝不自己翻 killed。
72
+ * · 顶层 durable run 行:`runs.cancel(taskId)`(包内 `controlRouter.cancel`,202 ack)。
73
+ * 🔴 但 **park 住的行不能走它**:suspended/needs_review 行要 cancel-by-DENY
74
+ * (`src/hitl/hitlBridge.ts:283` / `src/hitl/askGateWire.ts:417` 逐字纪律)——
75
+ * 所以「行在等人」(status==='waiting-on-human')时 stop 判别必须映射到决断面而不是 cancel。
76
+ * · workflow 行:**今天包/SDK 面上没有 workflow 的停止 verb** —— 实装 SDK 2.0.0 的
77
+ * `WorkflowsResource` 只有 list/get/journal/stream/steerAgent(逐个核过);而 `runs.d.ts:224`
78
+ * 写着「a workflow handle 404s — stop workflows via their cancel face」,那个 "cancel face"
79
+ * **不在 SDK 公面上**。⇒ 下一批第一步是找 server 把它钉出来(是 `runs.cancel(w* runId)`
80
+ * 还是独立端点),🔴 在拿到 server 的确证之前不许猜、不许在端上摆 Stop。
81
+ * ③ **不可停时端凭什么显隐**:凭①的键缺席,而**不是**凭 status —— 例如 'stopping' 行是「已经在
82
+ * 停」(不该再给 Stop),park 行是「要走决断面」,老引擎无 taskHandles 是「面不在」,三者语义
83
+ * 不同但都落在「不给 Stop」上;把它们压进一个 status 判断会在下一次加词时静默错。
35
84
  */
36
85
  import type { AgentClient, AssistantTask, FleetTaskRow, FleetTaskStatus } from '@sema-agent/sdk';
37
86
  import type { BackgroundRow, BackgroundSourceHealth, BackgroundStatus, BackgroundView, CreateBackgroundView } from './contract.js';
@@ -43,7 +92,9 @@ export declare function backgroundStatusFromAssistant(s: AssistantTask['status']
43
92
  * 'running'(诚实取舍:它既不 queued 也不终态,更不是在等人)。 */
44
93
  export declare function backgroundStatusFromFleet(s: FleetTaskStatus): BackgroundStatus;
45
94
  /** 一行 AssistantTask → BackgroundRow。gate 是 `AssistantGate | null`(wire 显式 null 形)——
46
- * null 折成键缺席,非 null 原引用透传(契约位是 unknown,包不重构造)。 */
95
+ * null 折成键缺席,非 null 原引用透传(契约位是 unknown,包不重构造)。
96
+ * 🔴 [C33]③.1 三键(name / sessionId / createdAtMs)按 wire 在场性投影:缺席=键缺席,绝不
97
+ * `?? 0` / `?? ''` —— 端渲的是「不知道」,不是「0 秒前创建的无名任务」。 */
47
98
  export declare function assistantTaskToBackgroundRow(t: AssistantTask): BackgroundRow;
48
99
  /** 一行 FleetTaskRow → BackgroundRow;子行(parentId 在场)⇒ null(调用方剔除)。
49
100
  * 复合 id 不拆;`currentTool` 契约位是一行字符串 —— 优先 server 已 redact 的 `currentAction`
@@ -56,15 +56,28 @@ function isoToEpochMs(iso) {
56
56
  const ms = Date.parse(iso);
57
57
  return Number.isFinite(ms) ? ms : undefined;
58
58
  }
59
+ /** 非空串才算「有值」:空串是「没有名字」,不是名字(占位归端)。 */
60
+ function nonEmptyString(v) {
61
+ return typeof v === 'string' && v !== '' ? v : undefined;
62
+ }
59
63
  /** 一行 AssistantTask → BackgroundRow。gate 是 `AssistantGate | null`(wire 显式 null 形)——
60
- * null 折成键缺席,非 null 原引用透传(契约位是 unknown,包不重构造)。 */
64
+ * null 折成键缺席,非 null 原引用透传(契约位是 unknown,包不重构造)。
65
+ * 🔴 [C33]③.1 三键(name / sessionId / createdAtMs)按 wire 在场性投影:缺席=键缺席,绝不
66
+ * `?? 0` / `?? ''` —— 端渲的是「不知道」,不是「0 秒前创建的无名任务」。 */
61
67
  export function assistantTaskToBackgroundRow(t) {
68
+ const wire = t;
62
69
  const updatedAtMs = isoToEpochMs(t.updatedAt);
70
+ const createdAtMs = isoToEpochMs(t.createdAt);
71
+ const name = nonEmptyString(wire.name);
72
+ const sessionId = nonEmptyString(t.sessionId);
63
73
  return {
64
74
  key: { source: 'assistant', taskId: t.taskId },
65
75
  status: backgroundStatusFromAssistant(t.status),
66
76
  ...(updatedAtMs !== undefined ? { updatedAtMs } : {}),
77
+ ...(createdAtMs !== undefined ? { createdAtMs } : {}),
78
+ ...(sessionId !== undefined ? { sessionId } : {}),
67
79
  needsAttention: t.needsAttention,
80
+ ...(name !== undefined ? { name } : {}),
68
81
  ...(t.gate !== null ? { gate: t.gate } : {}),
69
82
  };
70
83
  }
@@ -81,9 +94,14 @@ export function fleetTaskToBackgroundRow(r) {
81
94
  ? `${r.currentTool.toolName}${r.currentTool.target !== undefined ? ` ${r.currentTool.target}` : ''}`
82
95
  : undefined;
83
96
  const parked = r.status === 'awaiting approval' || r.status === 'waiting' || r.awaitingPlanApproval === true;
97
+ // [C33]③.2 / server [C36]:launch 时刻 epoch ms(revive 行=本 revive 周期 wake 时刻),
98
+ // ADDITIVE / tolerate-absent —— 排队中的行本就没有,缺席即键缺席(0 不是「1970 年启动」)。
99
+ // 两腿同一个 mapper:`snapshot` 首帧行与 `task` 增量帧行(POST-MERGE FULL row)都过这里。
100
+ const startedAtMs = typeof r.startedAt === 'number' && r.startedAt > 0 ? r.startedAt : undefined;
84
101
  return {
85
102
  key: { source: 'fleet', rowId: r.id },
86
103
  status: backgroundStatusFromFleet(r.status),
104
+ ...(startedAtMs !== undefined ? { startedAtMs } : {}),
87
105
  ...(parked ? { needsAttention: true } : {}),
88
106
  ...(typeof r.name === 'string' && r.name !== '' ? { name: r.name } : {}),
89
107
  ...(typeof r.description === 'string' && r.description !== '' ? { description: r.description } : {}),
@@ -132,8 +150,8 @@ export function createBackgroundView(client, opts) {
132
150
  let disposed = false;
133
151
  let current = null;
134
152
  let lastSerialized = null;
135
- /** caps 探针结果:null = 还没探到/caps 面不可达(判别缺席);boolean = scheduler 位。 */
136
- let capsScheduler = null;
153
+ /** caps 探针结果:null = 还没探到/caps 面不可达(判别缺席);boolean = approvals 位(见头注件 3)。 */
154
+ let capsApprovals = null;
137
155
  const listeners = new Set();
138
156
  const abort = new AbortController();
139
157
  function notify() {
@@ -148,26 +166,30 @@ export function createBackgroundView(client, opts) {
148
166
  }
149
167
  }
150
168
  }
151
- /** caps 只需探到一次;失败保持 null 下轮重试(caps 面挂 ≠ assistant 面挂,判别缺席不编造)。 */
169
+ /** caps 只需探到一次;失败保持 null 下轮重试(caps 面挂 ≠ assistant 面挂,判别缺席不编造)。
170
+ * 🔴 读的是 `approvals` 不是 `scheduler` —— 考据见头注「'not-configured' 判别」节件 3。 */
152
171
  async function probeCaps() {
153
- if (capsScheduler !== null)
172
+ if (capsApprovals !== null)
154
173
  return;
155
174
  try {
156
175
  const caps = await client.capabilities({ signal: abort.signal });
157
- capsScheduler = caps.scheduler === true;
176
+ capsApprovals = caps.approvals === true;
158
177
  }
159
178
  catch (err) {
160
- debugLog(`caps probe unavailable (scheduler 判别缺席,保持 ok 口径): ${String(err)}`);
179
+ debugLog(`caps probe unavailable (approvals 判别缺席,保持 ok 口径): ${String(err)}`);
161
180
  }
162
181
  }
163
182
  async function pullAssistant() {
164
183
  try {
165
184
  const { tasks } = await client.assistant.tasks({ signal: abort.signal });
166
- return { health: capsScheduler === false ? 'not-configured' : 'ok', tasks: tasks ?? [] };
185
+ return { health: capsApprovals === false ? 'not-configured' : 'ok', tasks: tasks ?? [] };
167
186
  }
168
187
  catch (err) {
169
188
  debugLog(`assistant.tasks degrade: ${String(err)}`);
170
- const health = capsScheduler === false || isNotImplemented(err) ? 'not-configured' : 'unavailable';
189
+ // 🔴 只有 approvals:false(面本就没挂)与 501(路由自报 gated off) not-configured
190
+ // 「caps 说有(approvals:true)却 404」**留在 unavailable** —— 那是两个真源对不上的报警,
191
+ // 吞掉它以后真的漏注册/误删路由就再也看不见了(server [C34] 明确否掉的修法 (a))。
192
+ const health = capsApprovals === false || isNotImplemented(err) ? 'not-configured' : 'unavailable';
171
193
  return { health, tasks: [] };
172
194
  }
173
195
  }
@@ -130,6 +130,33 @@ export interface BackgroundRow {
130
130
  readonly status: BackgroundStatus;
131
131
  /** 统一 epoch ms;assistant 的 ISO 串只在边界转一次。 */
132
132
  readonly updatedAtMs?: number;
133
+ /**
134
+ * 行**创建**时刻(统一 epoch ms)。语义 = 这一行是什么时候被排上的,不是什么时候开跑的。
135
+ * · assistant 臂:`AssistantTask.createdAt`(wire 是 ISO 串,与 updatedAt 同一个边界转换器);
136
+ * · fleet 臂:wire 上**没有这个量** ⇒ fleet 行恒缺席(fleet 给的是 {@link startedAtMs})。
137
+ * 缺席后果:端渲不出「排了多久」,必须不渲 —— 🔴 绝不 `?? Date.now()`/`?? 0` 冒充,也绝不拿
138
+ * updatedAtMs 顶替(那是「最后一次动」,不是「什么时候来的」)。
139
+ */
140
+ readonly createdAtMs?: number;
141
+ /**
142
+ * 行**开跑**时刻(统一 epoch ms)= elapsed 的锚。
143
+ * · fleet 臂:`FleetTaskRow.startedAt`(server ≥1.250,fleet-bus.ts:94;launch 时刻,revive 行
144
+ * = 本 revive 周期的 wake 时刻)。契约级别 **ADDITIVE / tolerate-absent** —— 排队中的行本就
145
+ * 没有它(server 侧据此把显示态落 queued),消费端**不能当必填**。
146
+ * · assistant 臂:wire 上没有 launch 时刻 ⇒ assistant 行恒缺席(它给的是 {@link createdAtMs})。
147
+ * 为什么是它而不是 server 的 `elapsedMs`:后者是发帧那一刻的快照,客户端拿着自算会漂;
148
+ * 锚是绝对时刻才稳(server [C36])。缺席后果:端**不渲时长**(假 0s 比不渲更坏)。
149
+ */
150
+ readonly startedAtMs?: number;
151
+ /**
152
+ * 这一行所属的引擎会话 id。语义 = 「跳到该会话 / 按会话归组」这类 affordance 的目标。
153
+ * · assistant 臂:`AssistantTask.sessionId`(wire 恒在场);
154
+ * · fleet 臂:server 在 `stripT`(routes/fleet.ts:95)里把 `sessionId`/`scope` 剥掉不上 wire
155
+ * ⇒ fleet 行恒缺席,别在投影里等它([C36] 事实②)。
156
+ * 缺席后果:那条跳转/归组的 affordance **必须显隐** —— 没有已挣得的目标就不摆入口
157
+ * (假 affordance 比没有更坏);🔴 绝不 `?? ''` 造一个空会话 id 出来。
158
+ */
159
+ readonly sessionId?: string;
133
160
  readonly needsAttention?: boolean;
134
161
  readonly name?: string;
135
162
  readonly description?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.11.18",
3
+ "version": "0.11.19",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",