@sema-agent/client-core 0.11.15 → 0.11.17

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.
@@ -17,6 +17,9 @@ export interface FleetAgentRowLike {
17
17
  toolName: string;
18
18
  target?: string;
19
19
  };
20
+ /** server 1.250 起行带的 launch 真锚(epoch ms)。R4(clay 五报 07-31):此前投影载荷丢弃它,
21
+ * 详情页 elapsed 只能锚「壳建行时刻」——遗留行上渲出 1h+ 的假时长。缺席=老引擎,消费端保持现状。 */
22
+ startedAt?: number;
20
23
  }
21
24
  /** 行从投影集消失多久之后按 completed 兜底 settle(重连空窗 / snapshot 清表远小于此)。 */
22
25
  export declare const ABSENT_SETTLE_MS = 30000;
@@ -80,6 +80,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
80
80
  // 🔴 三态直取:`row.toolUses` 缺席就是 undefined,这里**不许**兜 0(见 FleetAgentRowLike 注)。
81
81
  const toolUses = typeof row.toolUses === 'number' ? row.toolUses : undefined;
82
82
  const transcriptId = row.transcriptId || undefined;
83
+ const startedAt = typeof row.startedAt === 'number' && row.startedAt > 0 ? row.startedAt : undefined;
83
84
  const currentTool = currentToolKeyOf(row.currentTool) !== undefined ? row.currentTool : undefined;
84
85
  const currentToolKey = currentToolKeyOf(row.currentTool);
85
86
  const prev = seen.get(taskId);
@@ -89,7 +90,8 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
89
90
  // 终态行也把最后一拍的累计工具数/转录锚带上:详情页在行翻终态后仍要显示真计数与
90
91
  // 委派 prompt,而终态帧常常是这一行**唯一**带全 usage 的一帧(bg lane 只在活动边界发行帧)。
91
92
  if ((toolUses !== undefined && prev?.toolUses !== toolUses) ||
92
- (transcriptId !== undefined && prev?.transcriptId !== transcriptId)) {
93
+ (transcriptId !== undefined && prev?.transcriptId !== transcriptId) ||
94
+ (startedAt !== undefined && prev?.startedAt !== startedAt)) {
93
95
  publishEngineAgentPanelEvent({
94
96
  kind: 'fleet-row',
95
97
  taskId,
@@ -98,6 +100,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
98
100
  totalTokens: tokens,
99
101
  ...(toolUses !== undefined ? { toolUses } : {}),
100
102
  ...(transcriptId !== undefined ? { transcriptId } : {}),
103
+ ...(startedAt !== undefined ? { startedAt } : {}),
101
104
  });
102
105
  }
103
106
  publishEngineAgentPanelEvent({
@@ -111,6 +114,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
111
114
  tokens,
112
115
  toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
113
116
  transcriptId: transcriptId !== undefined ? transcriptId : prev?.transcriptId,
117
+ startedAt: startedAt !== undefined ? startedAt : prev?.startedAt,
114
118
  currentToolKey: currentToolKey !== undefined ? currentToolKey : prev?.currentToolKey,
115
119
  lastSeenAt: nowMs,
116
120
  settled: true,
@@ -127,6 +131,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
127
131
  prev.tokens !== tokens ||
128
132
  (toolUses !== undefined && prev.toolUses !== toolUses) ||
129
133
  (transcriptId !== undefined && prev.transcriptId !== transcriptId) ||
134
+ (startedAt !== undefined && prev.startedAt !== startedAt) ||
130
135
  (currentToolKey !== undefined && prev.currentToolKey !== currentToolKey)) {
131
136
  publishEngineAgentPanelEvent({
132
137
  kind: 'fleet-row',
@@ -137,6 +142,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
137
142
  // 🔴 缺席即不落键(消费端据「键在不在」判三态)。
138
143
  ...(toolUses !== undefined ? { toolUses } : {}),
139
144
  ...(transcriptId !== undefined ? { transcriptId } : {}),
145
+ ...(startedAt !== undefined ? { startedAt } : {}),
140
146
  ...(currentTool !== undefined ? { currentTool } : {}),
141
147
  });
142
148
  }
@@ -146,6 +152,7 @@ export function projectFleetAgentRows(rows, nowMs = Date.now()) {
146
152
  tokens,
147
153
  toolUses: toolUses !== undefined ? toolUses : prev?.toolUses,
148
154
  transcriptId: transcriptId !== undefined ? transcriptId : prev?.transcriptId,
155
+ startedAt: startedAt !== undefined ? startedAt : prev?.startedAt,
149
156
  currentToolKey: currentToolKey !== undefined ? currentToolKey : prev?.currentToolKey,
150
157
  lastSeenAt: nowMs,
151
158
  settled: false,
@@ -77,8 +77,14 @@ export interface HitlHostSurface {
77
77
  }
78
78
  /** 装 HITL 宿主面(传 null 卸)。返回还原函数。 */
79
79
  export declare function installHitlHostSurface(surface: HitlHostSurface | null): () => void;
80
+ /** W1 带 key 变体:装/卸某会话的 HITL 宿主面(多会话宿主每会话一键,互不顶盖)。 */
81
+ export declare function installHitlHostSurfaceFor(sessionKey: string, surface: HitlHostSurface | null): () => void;
80
82
  /** 🔴 宿主自检:恒应为 0。非 0 = 有 HITL 副作用发生时口不在,那一行 warn / 那条记账丢了。 */
81
83
  export declare function hitlHostSurfaceMisses(): number;
84
+ /** W1 带 key 变体(miss 计数逐键)。 */
85
+ export declare function hitlHostSurfaceMissesFor(sessionKey: string): number;
86
+ /** W1 读端:某会话的 HITL 宿主面(未装 = null;**不计 miss** —— miss 只在副作用真发生时计)。 */
87
+ export declare function hitlHostSurfaceFor(sessionKey: string): HitlHostSurface | null;
82
88
  /** 测试钩:卸口 + 清计数。 */
83
89
  export declare function _resetHitlHostSurfaceForTest(): void;
84
90
  /**
@@ -1,30 +1,46 @@
1
1
  import { HitlBridge } from './hitlBridge.js';
2
2
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
3
3
  import { hostLog } from '../host.js';
4
- import { isFsApprovalGate, toolNameIsFsWrite, surfaceFsApprovalAndDecide, isToolApprovalFrame, isFromSubagent, surfaceToolApprovalFrameAndRespond, } from './toolApprovalWire.js';
4
+ import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
5
+ import { isToolApprovalGate, toolNameIsFsWrite, surfaceFsApprovalAndDecide, isToolApprovalFrame, isFromSubagent, surfaceToolApprovalFrameAndRespond, } from './toolApprovalWire.js';
5
6
  import { classifierDenyFromToolEnd } from '../classifierVerdictWire.js';
6
- let hostSurface = null;
7
- let hostSurfaceMisses = 0;
7
+ // W1(design/161):sessionKey → 注册表;零参 API = DEFAULT_SESSION_KEY 兼容层(cli 装配不动)。
8
+ const hostSurfaceByKey = createSessionSlot();
9
+ const hostSurfaceMissesByKey = new Map();
8
10
  /** 装 HITL 宿主面(传 null 卸)。返回还原函数。 */
9
11
  export function installHitlHostSurface(surface) {
10
- const prev = hostSurface;
11
- hostSurface = surface;
12
+ return installHitlHostSurfaceFor(DEFAULT_SESSION_KEY, surface);
13
+ }
14
+ /** W1 带 key 变体:装/卸某会话的 HITL 宿主面(多会话宿主每会话一键,互不顶盖)。 */
15
+ export function installHitlHostSurfaceFor(sessionKey, surface) {
16
+ const prev = hostSurfaceByKey.get(sessionKey);
17
+ hostSurfaceByKey.set(sessionKey, surface);
12
18
  return () => {
13
- hostSurface = prev;
19
+ hostSurfaceByKey.set(sessionKey, prev);
14
20
  };
15
21
  }
16
22
  /** 🔴 宿主自检:恒应为 0。非 0 = 有 HITL 副作用发生时口不在,那一行 warn / 那条记账丢了。 */
17
23
  export function hitlHostSurfaceMisses() {
18
- return hostSurfaceMisses;
24
+ return hitlHostSurfaceMissesFor(DEFAULT_SESSION_KEY);
25
+ }
26
+ /** W1 带 key 变体(miss 计数逐键)。 */
27
+ export function hitlHostSurfaceMissesFor(sessionKey) {
28
+ return hostSurfaceMissesByKey.get(sessionKey) ?? 0;
29
+ }
30
+ /** W1 读端:某会话的 HITL 宿主面(未装 = null;**不计 miss** —— miss 只在副作用真发生时计)。 */
31
+ export function hitlHostSurfaceFor(sessionKey) {
32
+ return hostSurfaceByKey.get(sessionKey) ?? null;
19
33
  }
20
34
  /** 测试钩:卸口 + 清计数。 */
21
35
  export function _resetHitlHostSurfaceForTest() {
22
- hostSurface = null;
23
- hostSurfaceMisses = 0;
36
+ hostSurfaceByKey.clear();
37
+ hostSurfaceMissesByKey.clear();
24
38
  }
25
39
  function surface() {
26
- if (hostSurface === null)
27
- hostSurfaceMisses++;
40
+ const hostSurface = hitlHostSurfaceFor(DEFAULT_SESSION_KEY);
41
+ if (hostSurface === null) {
42
+ hostSurfaceMissesByKey.set(DEFAULT_SESSION_KEY, (hostSurfaceMissesByKey.get(DEFAULT_SESSION_KEY) ?? 0) + 1);
43
+ }
28
44
  return hostSurface;
29
45
  }
30
46
  /**
@@ -53,8 +69,9 @@ function isAskGatePark(result) {
53
69
  typeof r.taskId === 'string' &&
54
70
  r.taskId.length > 0);
55
71
  }
56
- /** done 帧的 fs 写权限 park 形状([816] 放宽腿):status suspended + checkpointGate 指 fs 写工具
57
- * (Write/Edit/NotebookEdit)或一等 kind==='tool_approval'。Ask gate 恒先判(问答 overlay 原路)。 */
72
+ /** done 帧的工具审批 park 形状([816] 放宽腿 + [2150] Bash 半场):status suspended +
73
+ * checkpointGate 指 fs 写三件 / Bash / 一等 kind==='tool_approval'(超集谓词 isToolApprovalGate,
74
+ * decide 腿对两族同构、卡形归壳分发)。Ask gate 恒先判(问答 overlay 原路)。 */
58
75
  function isFsApprovalPark(result) {
59
76
  const r = result;
60
77
  return (!!r &&
@@ -62,7 +79,7 @@ function isFsApprovalPark(result) {
62
79
  typeof r.taskId === 'string' &&
63
80
  r.taskId.length > 0 &&
64
81
  !isAskTool(r.checkpointGate?.toolName) &&
65
- isFsApprovalGate(r.checkpointGate));
82
+ isToolApprovalGate(r.checkpointGate));
66
83
  }
67
84
  /** 把 wire 答案({answers:[{header,selected,note?}]})折回 CC 卡片的 Record<question,string> 形状
68
85
  * (multiSelect 与对话框同款 ", " lossy join;note → annotations.notes)。 */
@@ -461,7 +478,7 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
461
478
  park = { gate: 'ask' };
462
479
  break;
463
480
  }
464
- if (ev.gate && isFsApprovalGate(ev.gate) && realTaskId) {
481
+ if (ev.gate && isToolApprovalGate(ev.gate) && realTaskId) {
465
482
  park = { gate: 'fs' };
466
483
  break;
467
484
  }
@@ -6,6 +6,17 @@ export declare function isFsApprovalGate(gate: {
6
6
  toolName?: unknown;
7
7
  } | undefined | null): boolean;
8
8
  export declare function toolNameIsFsWrite(name: string): boolean;
9
+ /** shell 执行工具判定(Bash)。[2150] S1 半场:引擎对 Bash 发 durable park 时,fs 谓词不认 ⇒
10
+ * park 落「其余 gate 透传」= run 留 suspended、session 锁死(r1 E5 fail-stuck 实例)。 */
11
+ export declare function toolNameIsShellExec(name: string): boolean;
12
+ /** 工具审批 gate 判定(durable park 分诊用的**超集**谓词):一等 kind / fs 写三件 / shell 执行。
13
+ * decide 腿(approvals.decide)对两族同构;卡形分发归壳 ApprovalCardPort(toolForName 已含
14
+ * bash → BashPermissionRequest 映射,sema-cli a35e2b2)。`isFsApprovalGate` 保留原语义不动
15
+ * (公开面消费者按 fs 写理解它,把 Bash 塞进去是撒谎)。 */
16
+ export declare function isToolApprovalGate(gate: {
17
+ kind?: unknown;
18
+ toolName?: unknown;
19
+ } | undefined | null): boolean;
9
20
  export type FsApprovalOutcome = {
10
21
  kind: 'decided';
11
22
  gatedCallId?: string;
@@ -61,10 +72,18 @@ export interface ApprovalCardRequest {
61
72
  export type ApprovalCardPort = (req: ApprovalCardRequest) => Promise<ApprovalCardDecision>;
62
73
  /** 装卡口(传 null 卸)。返回还原函数。 */
63
74
  export declare function installApprovalCardPort(port: ApprovalCardPort | null): () => void;
75
+ /** W1 带 key 变体:装/卸某会话的卡口(多会话宿主每会话一键,互不顶盖)。 */
76
+ export declare function installApprovalCardPortFor(sessionKey: string, port: ApprovalCardPort | null): () => void;
64
77
  /** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
65
78
  export declare function approvalCardPortMisses(): number;
79
+ /** W1 带 key 变体(miss 计数逐键)。 */
80
+ export declare function approvalCardPortMissesFor(sessionKey: string): number;
66
81
  /** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
67
82
  export declare function hasApprovalCardPort(): boolean;
83
+ /** W1 带 key 变体。 */
84
+ export declare function hasApprovalCardPortFor(sessionKey: string): boolean;
85
+ /** W1 读端:某会话的卡口(未装 = null;**不计 miss** —— miss 只在决断腿真到达时计)。 */
86
+ export declare function approvalCardPortFor(sessionKey: string): ApprovalCardPort | null;
68
87
  /** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
69
88
  export declare function _resetApprovalCardPortForTest(): void;
70
89
  /**
@@ -1,5 +1,6 @@
1
1
  import { HitlBridge } from './hitlBridge.js';
2
2
  import { hostLog } from '../host.js';
3
+ import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
3
4
  import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
4
5
  /** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
5
6
  * [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
@@ -14,6 +15,20 @@ export function toolNameIsFsWrite(name) {
14
15
  const n = name.replace(/[\s_-]+/g, '').toLowerCase();
15
16
  return (n === 'write' || n === 'filewrite' || n === 'edit' || n === 'fileedit' || n === 'multiedit' || n === 'notebookedit');
16
17
  }
18
+ /** shell 执行工具判定(Bash)。[2150] S1 半场:引擎对 Bash 发 durable park 时,fs 谓词不认 ⇒
19
+ * park 落「其余 gate 透传」= run 留 suspended、session 锁死(r1 E5 fail-stuck 实例)。 */
20
+ export function toolNameIsShellExec(name) {
21
+ return name.replace(/[\s_-]+/g, '').toLowerCase() === 'bash';
22
+ }
23
+ /** 工具审批 gate 判定(durable park 分诊用的**超集**谓词):一等 kind / fs 写三件 / shell 执行。
24
+ * decide 腿(approvals.decide)对两族同构;卡形分发归壳 ApprovalCardPort(toolForName 已含
25
+ * bash → BashPermissionRequest 映射,sema-cli a35e2b2)。`isFsApprovalGate` 保留原语义不动
26
+ * (公开面消费者按 fs 写理解它,把 Bash 塞进去是撒谎)。 */
27
+ export function isToolApprovalGate(gate) {
28
+ if (isFsApprovalGate(gate))
29
+ return true;
30
+ return typeof gate?.toolName === 'string' && toolNameIsShellExec(gate.toolName);
31
+ }
17
32
  /** 结构等值(键序无关深比较)——updatedInput「真编辑过」判定用。zod parse 会产新引用与重排,
18
33
  * 引用比较/JSON.stringify 串比较都会假报「编辑过」。 */
19
34
  export function structurallyEqual(a, b) {
@@ -36,28 +51,48 @@ export function structurallyEqual(a, b) {
36
51
  return false;
37
52
  return ka.every(k => structurallyEqual(a[k], b[k]));
38
53
  }
39
- let cardPort = null;
40
- let cardPortMisses = 0;
54
+ // W1(design/161):sessionKey → 注册表;零参 API = DEFAULT_SESSION_KEY 兼容层(cli 装配不动)。
55
+ const cardPortByKey = createSessionSlot();
56
+ const cardPortMissesByKey = new Map();
57
+ function bumpCardPortMiss(sessionKey) {
58
+ cardPortMissesByKey.set(sessionKey, (cardPortMissesByKey.get(sessionKey) ?? 0) + 1);
59
+ }
41
60
  /** 装卡口(传 null 卸)。返回还原函数。 */
42
61
  export function installApprovalCardPort(port) {
43
- const prev = cardPort;
44
- cardPort = port;
62
+ return installApprovalCardPortFor(DEFAULT_SESSION_KEY, port);
63
+ }
64
+ /** W1 带 key 变体:装/卸某会话的卡口(多会话宿主每会话一键,互不顶盖)。 */
65
+ export function installApprovalCardPortFor(sessionKey, port) {
66
+ const prev = cardPortByKey.get(sessionKey);
67
+ cardPortByKey.set(sessionKey, port);
45
68
  return () => {
46
- cardPort = prev;
69
+ cardPortByKey.set(sessionKey, prev);
47
70
  };
48
71
  }
49
72
  /** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
50
73
  export function approvalCardPortMisses() {
51
- return cardPortMisses;
74
+ return approvalCardPortMissesFor(DEFAULT_SESSION_KEY);
75
+ }
76
+ /** W1 带 key 变体(miss 计数逐键)。 */
77
+ export function approvalCardPortMissesFor(sessionKey) {
78
+ return cardPortMissesByKey.get(sessionKey) ?? 0;
52
79
  }
53
80
  /** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
54
81
  export function hasApprovalCardPort() {
55
- return cardPort !== null;
82
+ return hasApprovalCardPortFor(DEFAULT_SESSION_KEY);
83
+ }
84
+ /** W1 带 key 变体。 */
85
+ export function hasApprovalCardPortFor(sessionKey) {
86
+ return approvalCardPortFor(sessionKey) !== null;
87
+ }
88
+ /** W1 读端:某会话的卡口(未装 = null;**不计 miss** —— miss 只在决断腿真到达时计)。 */
89
+ export function approvalCardPortFor(sessionKey) {
90
+ return cardPortByKey.get(sessionKey) ?? null;
56
91
  }
57
92
  /** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
58
93
  export function _resetApprovalCardPortForTest() {
59
- cardPort = null;
60
- cardPortMisses = 0;
94
+ cardPortByKey.clear();
95
+ cardPortMissesByKey.clear();
61
96
  }
62
97
  /**
63
98
  * 共享卡面:弹 vendored CC 三选卡(PermissionRequest)→ 等用户决断。两条腿共用:
@@ -67,8 +102,9 @@ export function _resetApprovalCardPortForTest() {
67
102
  * (accept-session 升 intent,[816] 壳侧承诺② —— 与决断通道无关,两腿都要)由**宿主卡口**做。
68
103
  */
69
104
  export function surfaceApprovalCard(req) {
105
+ const cardPort = approvalCardPortFor(DEFAULT_SESSION_KEY);
70
106
  if (cardPort === null) {
71
- cardPortMisses++;
107
+ bumpCardPortMiss(DEFAULT_SESSION_KEY);
72
108
  return Promise.resolve({
73
109
  kind: 'failed',
74
110
  reason: 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)',
@@ -85,7 +121,7 @@ export function surfaceApprovalCard(req) {
85
121
  */
86
122
  export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signal) {
87
123
  if (!hasApprovalCardPort()) {
88
- cardPortMisses++;
124
+ bumpCardPortMiss(DEFAULT_SESSION_KEY);
89
125
  return { kind: 'failed', reason: 'no approval card port installed (print/non-REPL mode)' };
90
126
  }
91
127
  // pending 行 = gate 的权威源 + D-1 绑定 surface(与 ask 桥同姿势)。
@@ -93,7 +129,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
93
129
  try {
94
130
  const rows = (await deps.client.approvals.list(signal ? { signal } : undefined)).pending;
95
131
  pending =
96
- rows.find(r => r.taskId === taskId && typeof r.toolName === 'string' && toolNameIsFsWrite(r.toolName)) ??
132
+ rows.find(r => r.taskId === taskId && typeof r.toolName === 'string' && (toolNameIsFsWrite(r.toolName) || toolNameIsShellExec(r.toolName))) ??
97
133
  rows.find(r => r.taskId === taskId);
98
134
  }
99
135
  catch (e) {
package/dist/host.d.ts CHANGED
@@ -96,23 +96,39 @@ export interface HostPorts {
96
96
  * 返回一个还原函数(测试/热重载用)。
97
97
  */
98
98
  export declare function installHost(ports: HostPorts): () => void;
99
+ /**
100
+ * W1 带 key 变体:装配某会话的宿主口(多会话宿主每会话一键,互不顶盖;合并语义与零参同)。
101
+ * ⚠️ 入参不含 `queue` —— 通知队列在 W1 仍是包级单例(notifications.ts 不在五槽位切口内),
102
+ * per-session 队列归后续工单;要装队列走零参 `installHost({ queue })`。
103
+ */
104
+ export declare function installHostFor(sessionKey: string, ports: Omit<HostPorts, 'queue'>): () => void;
99
105
  /** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
100
106
  export declare function hostLog(level: AdapterLogLevel, msg: string, data?: unknown): void;
101
107
  /** 行式探针(缺席 = 静默,不计 miss:同上)。 */
102
108
  export declare function hostProbe(channel: string, line: string): void;
103
109
  /** settings 口(缺席 = undefined + 计 miss:漏装会让 hooks 整条 wire 静默空掉)。 */
104
110
  export declare function hostSettings(): SettingsPort | undefined;
111
+ /** W1 带 key 变体(缺席语义与零参同:undefined + 计该键 miss)。 */
112
+ export declare function hostSettingsFor(sessionKey: string): SettingsPort | undefined;
105
113
  /** fs 口(缺席 = undefined + 计 miss)。 */
106
114
  export declare function hostFs(): FsPort | undefined;
115
+ /** W1 带 key 变体。 */
116
+ export declare function hostFsFor(sessionKey: string): FsPort | undefined;
107
117
  /** 定时器口(缺席 = undefined,不计 miss:T40 竞速本就允许不启用)。 */
108
118
  export declare function hostTimers(): TimersPort | undefined;
119
+ /** W1 带 key 变体(同零参:不计 miss)。 */
120
+ export declare function hostTimersFor(sessionKey: string): TimersPort | undefined;
109
121
  /** session 口(缺席 = undefined + 计 miss:漏装会让整条 durable 子代读面 404 静默哑掉)。 */
110
122
  export declare function hostSession(): SessionPort | undefined;
123
+ /** W1 带 key 变体。 */
124
+ export declare function hostSessionFor(sessionKey: string): SessionPort | undefined;
111
125
  /**
112
126
  * 🔴 宿主自检出口 —— 恒应为空对象。非空 = 有口漏装,**对应的面已经在静默失效**。
113
127
  * 含通知队列的 miss(从 notifications.ts 汇过来,宿主只需看这一个数)。
114
128
  */
115
129
  export declare function hostPortMisses(): Record<string, number>;
130
+ /** W1 带 key 变体。⚠️ 通知队列是包级单例,其 miss 只折进 DEFAULT_SESSION_KEY 键的视图。 */
131
+ export declare function hostPortMissesFor(sessionKey: string): Record<string, number>;
116
132
  /**
117
133
  * 测试钩:清空装配与计数。
118
134
  * 🔴 队列那一路必须走 `_resetNotificationQueuePortForTest()` 而不是 `installNotificationQueuePort(null)`
package/dist/host.js CHANGED
@@ -1,11 +1,19 @@
1
+ import { DEFAULT_SESSION_KEY } from './sessionSlot.js';
1
2
  import { _resetNotificationQueuePortForTest, installNotificationQueuePort, notificationQueuePortMisses, } from './notifications.js';
2
- let host = {};
3
- const misses = Object.create(null);
4
- const warned = new Set();
5
- function miss(name) {
6
- misses[name] = (misses[name] ?? 0) + 1;
7
- if (!warned.has(name)) {
8
- warned.add(name);
3
+ const hostByKey = new Map();
4
+ function hostSlot(sessionKey) {
5
+ let slot = hostByKey.get(sessionKey);
6
+ if (!slot) {
7
+ slot = { state: {}, misses: Object.create(null), warned: new Set() };
8
+ hostByKey.set(sessionKey, slot);
9
+ }
10
+ return slot;
11
+ }
12
+ function miss(sessionKey, name) {
13
+ const slot = hostSlot(sessionKey);
14
+ slot.misses[name] = (slot.misses[name] ?? 0) + 1;
15
+ if (!slot.warned.has(name)) {
16
+ slot.warned.add(name);
9
17
  // eslint-disable-next-line no-console
10
18
  console.error(`[client-core] host port "${name}" NOT installed — the surface it drives is silently inert. ` +
11
19
  'Host must call installHost({ ' +
@@ -21,58 +29,90 @@ function miss(name) {
21
29
  * 返回一个还原函数(测试/热重载用)。
22
30
  */
23
31
  export function installHost(ports) {
24
- const prev = { ...host };
32
+ const restore = installHostFor(DEFAULT_SESSION_KEY, ports);
33
+ if ('queue' in ports)
34
+ installNotificationQueuePort(ports.queue ?? null);
35
+ return restore;
36
+ }
37
+ /**
38
+ * W1 带 key 变体:装配某会话的宿主口(多会话宿主每会话一键,互不顶盖;合并语义与零参同)。
39
+ * ⚠️ 入参不含 `queue` —— 通知队列在 W1 仍是包级单例(notifications.ts 不在五槽位切口内),
40
+ * per-session 队列归后续工单;要装队列走零参 `installHost({ queue })`。
41
+ */
42
+ export function installHostFor(sessionKey, ports) {
43
+ const slot = hostSlot(sessionKey);
44
+ const prev = { ...slot.state };
25
45
  if ('log' in ports)
26
- host.log = ports.log;
46
+ slot.state.log = ports.log;
27
47
  if ('probe' in ports)
28
- host.probe = ports.probe;
48
+ slot.state.probe = ports.probe;
29
49
  if ('timers' in ports)
30
- host.timers = ports.timers;
50
+ slot.state.timers = ports.timers;
31
51
  if ('settings' in ports)
32
- host.settings = ports.settings;
52
+ slot.state.settings = ports.settings;
33
53
  if ('fs' in ports)
34
- host.fs = ports.fs;
54
+ slot.state.fs = ports.fs;
35
55
  if ('session' in ports)
36
- host.session = ports.session;
37
- if ('queue' in ports)
38
- installNotificationQueuePort(ports.queue ?? null);
56
+ slot.state.session = ports.session;
39
57
  return () => {
40
- host = prev;
58
+ slot.state = prev;
41
59
  };
42
60
  }
43
61
  /** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
44
62
  export function hostLog(level, msg, data) {
45
- host.log?.(level, msg, data);
63
+ hostSlot(DEFAULT_SESSION_KEY).state.log?.(level, msg, data);
46
64
  }
47
65
  /** 行式探针(缺席 = 静默,不计 miss:同上)。 */
48
66
  export function hostProbe(channel, line) {
49
- host.probe?.(channel, line);
67
+ hostSlot(DEFAULT_SESSION_KEY).state.probe?.(channel, line);
50
68
  }
51
69
  /** settings 口(缺席 = undefined + 计 miss:漏装会让 hooks 整条 wire 静默空掉)。 */
52
70
  export function hostSettings() {
53
- return host.settings ?? miss('settings');
71
+ return hostSettingsFor(DEFAULT_SESSION_KEY);
72
+ }
73
+ /** W1 带 key 变体(缺席语义与零参同:undefined + 计该键 miss)。 */
74
+ export function hostSettingsFor(sessionKey) {
75
+ return hostSlot(sessionKey).state.settings ?? miss(sessionKey, 'settings');
54
76
  }
55
77
  /** fs 口(缺席 = undefined + 计 miss)。 */
56
78
  export function hostFs() {
57
- return host.fs ?? miss('fs');
79
+ return hostFsFor(DEFAULT_SESSION_KEY);
80
+ }
81
+ /** W1 带 key 变体。 */
82
+ export function hostFsFor(sessionKey) {
83
+ return hostSlot(sessionKey).state.fs ?? miss(sessionKey, 'fs');
58
84
  }
59
85
  /** 定时器口(缺席 = undefined,不计 miss:T40 竞速本就允许不启用)。 */
60
86
  export function hostTimers() {
61
- return host.timers;
87
+ return hostTimersFor(DEFAULT_SESSION_KEY);
88
+ }
89
+ /** W1 带 key 变体(同零参:不计 miss)。 */
90
+ export function hostTimersFor(sessionKey) {
91
+ return hostSlot(sessionKey).state.timers;
62
92
  }
63
93
  /** session 口(缺席 = undefined + 计 miss:漏装会让整条 durable 子代读面 404 静默哑掉)。 */
64
94
  export function hostSession() {
65
- return host.session ?? miss('session');
95
+ return hostSessionFor(DEFAULT_SESSION_KEY);
96
+ }
97
+ /** W1 带 key 变体。 */
98
+ export function hostSessionFor(sessionKey) {
99
+ return hostSlot(sessionKey).state.session ?? miss(sessionKey, 'session');
66
100
  }
67
101
  /**
68
102
  * 🔴 宿主自检出口 —— 恒应为空对象。非空 = 有口漏装,**对应的面已经在静默失效**。
69
103
  * 含通知队列的 miss(从 notifications.ts 汇过来,宿主只需看这一个数)。
70
104
  */
71
105
  export function hostPortMisses() {
72
- const out = { ...misses };
73
- const q = notificationQueuePortMisses();
74
- if (q > 0)
75
- out.queue = q;
106
+ return hostPortMissesFor(DEFAULT_SESSION_KEY);
107
+ }
108
+ /** W1 key 变体。⚠️ 通知队列是包级单例,其 miss 只折进 DEFAULT_SESSION_KEY 键的视图。 */
109
+ export function hostPortMissesFor(sessionKey) {
110
+ const out = { ...hostSlot(sessionKey).misses };
111
+ if (sessionKey === DEFAULT_SESSION_KEY) {
112
+ const q = notificationQueuePortMisses();
113
+ if (q > 0)
114
+ out.queue = q;
115
+ }
76
116
  return out;
77
117
  }
78
118
  /**
@@ -82,9 +122,6 @@ export function hostPortMisses() {
82
122
  * 让「未装配时 misses 为空」这类断言恒假(B4 首写时就撞上了)。
83
123
  */
84
124
  export function _resetHostForTest() {
85
- host = {};
86
- for (const k of Object.keys(misses))
87
- delete misses[k];
88
- warned.clear();
125
+ hostByKey.clear();
89
126
  _resetNotificationQueuePortForTest();
90
127
  }
package/dist/index.d.ts CHANGED
@@ -182,6 +182,7 @@ export * from './adapter/runStream.js';
182
182
  export * from './request/taskRequest.js';
183
183
  export * from './clientSlice.js';
184
184
  export * from './host.js';
185
+ export { DEFAULT_SESSION_KEY } from './sessionSlot.js';
185
186
  export * from './scratchpadWireCaps.js';
186
187
  export * from './toolResult.js';
187
188
  export * from './diff/patch.js';
@@ -217,3 +218,5 @@ export * from './hitl/approvalsFeed.js';
217
218
  export * from './compensations.js';
218
219
  export * from './request/printNotification.js';
219
220
  export * from './seatContract.js';
221
+ export * from './agentSession/contract.js';
222
+ export * from './agentSession/backgroundView.js';
package/dist/index.js CHANGED
@@ -197,6 +197,10 @@ export * from './clientSlice.js';
197
197
  // 包级宿主装配(per-turn 的 AdapterContext 之外的那一层:settings / fs / queue / timers)。
198
198
  // 🔴 `installNotificationQueuePort` 仍是队列的真源出口,`installHost({queue})` 只是直通它。
199
199
  export * from './host.js';
200
+ // W1(design/161):五个模块级单槽位的 per-session 注册表键。零参旧 API 全部等价于用这个键调
201
+ // `*For(sessionKey, …)` 变体;多会话宿主(desktop)每会话一键。`createSessionSlot` 是包内实现件,
202
+ // 刻意不出 barrel(公面只承诺键与各槽位的 *For API)。
203
+ export { DEFAULT_SESSION_KEY } from './sessionSlot.js';
200
204
  // B2 判给 B4 的宿主耦合件之一(小的那件,同时是 FsPort 的端到端证明)。
201
205
  export * from './scratchpadWireCaps.js';
202
206
  // ── B5 批:工具结果卡 B/D/E 层 + T20 客户端 diff(2026-07-27)────────────────────────────────
@@ -290,3 +294,11 @@ export * from './request/printNotification.js';
290
294
  // → `LOCAL_SESSIONS_SPEC` 键集(run-seat-contract-keys-test 运行期对账)。加 verb 忘了加 SPEC
291
295
  // 的后果不是报错而是 preload 不注册 channel、渲染端读到 undefined(假 affordance 家族)。
292
296
  export * from './seatContract.js';
297
+ // ── design/161 实现批 A:后台任务归一视图平行入口(2026-07-30)────────────────────────────────
298
+ // 契约收敛物 contract.ts 随首件实现落地转正常出口(「实现落地一件出口一件」——
299
+ // createBackgroundView 是第一件;portability 门 TYPE_ONLY_MODULES 同批摘除该登记)。
300
+ // contract.ts 仍是纯类型零运行时,`export *` 在 dist 里是空转发 —— 类型面从此进公面。
301
+ export * from './agentSession/contract.js';
302
+ // 两源(assistant.tasks 跨 session 调度总览 + fleet 常驻 SSE 快照)独立拉取独立降级的归一投影;
303
+ // 两张 status 映射表 never 卫兵穷举;'not-configured' 判别姿势见文件头注。
304
+ export * from './agentSession/backgroundView.js';
@@ -1,8 +1,3 @@
1
- /**
2
- * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/liveQuestionStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
- * 本模块自持 AskUserQuestion 的 wire 词汇(SDK 0.0.44 删掉了 questions 资源导出),搬入后这套
4
- * 类型随包发布 —— web/桌面端的 HITL 覆盖层与 TUI 共用同一份 QuestionFrame/QuestionAnswer 形。
5
- */
6
1
  /**
7
2
  * src/sema/liveQuestionStore.ts — the SIDE-CHANNEL for §4④ live-stream AskUserQuestion (the conversation-seam
8
3
  * twin of liveSessionStore.ts).
@@ -80,11 +75,18 @@ export declare function registerLocalQuestionResponder(id: string, fn: RespondFn
80
75
  * break the live stream drain (the run must keep flowing; a lost frame just headless-defaults the ask).
81
76
  */
82
77
  export declare function publishQuestionFrame(frame: QuestionFrame): void;
78
+ /** W1 keyed variant: publish to ONE session's overlay (multi-session hosts demux by their own key). */
79
+ export declare function publishQuestionFrameFor(sessionKey: string, frame: QuestionFrame): void;
83
80
  /** Whether a UI overlay is currently subscribed (liveHitlAskWire gates on this: print/non-REPL mode has no
84
81
  * dialog to park on, so the suspended terminal must fall through honestly instead of hanging the stream). */
85
82
  export declare function hasQuestionOverlay(): boolean;
83
+ /** W1 keyed variant. */
84
+ export declare function hasQuestionOverlayFor(sessionKey: string): boolean;
86
85
  /** Subscribe the UI overlay to live question frames. Returns an unsubscribe. Last writer wins (one overlay). */
87
86
  export declare function onQuestionFrame(handler: QuestionFrameHandler): () => void;
87
+ /** W1 keyed variant: one overlay PER sessionKey (last writer wins within a key; keys never clobber
88
+ * each other — the r1 E2 dual-session hazard this registry exists to close). */
89
+ export declare function onQuestionFrameFor(sessionKey: string, handler: QuestionFrameHandler): () => void;
88
90
  /** Publish the live client's `questions.respond` binding (called from createLiveConversationClient). `null`
89
91
  * clears it (offline/mock → respondToQuestion rejects cleanly). */
90
92
  export declare function setQuestionRespond(fn: RespondFn | null): void;