@sema-agent/client-core 0.11.14 → 0.11.16
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 +2 -1
- package/dist/agentSession/backgroundView.d.ts +76 -0
- package/dist/agentSession/backgroundView.js +254 -0
- package/dist/agentSession/contract.d.ts +152 -0
- package/dist/agentSession/contract.js +13 -0
- package/dist/controlRouter.d.ts +8 -0
- package/dist/controlRouter.js +4 -2
- package/dist/engineWireTarget.d.ts +7 -0
- package/dist/engineWireTarget.js +19 -5
- package/dist/hitl/askGateWire.d.ts +6 -0
- package/dist/hitl/askGateWire.js +26 -10
- package/dist/hitl/toolApprovalWire.d.ts +8 -0
- package/dist/hitl/toolApprovalWire.js +33 -11
- package/dist/host.d.ts +16 -0
- package/dist/host.js +68 -31
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/liveQuestionStore.d.ts +7 -5
- package/dist/liveQuestionStore.js +23 -42
- package/dist/request/taskRequest.d.ts +2 -0
- package/dist/request/taskRequest.js +5 -0
- package/dist/sessionSlot.d.ts +26 -0
- package/dist/sessionSlot.js +31 -0
- package/dist/workflowClient.d.ts +20 -0
- package/dist/workflowClient.js +55 -1
- package/package.json +1 -1
package/dist/hitl/askGateWire.js
CHANGED
|
@@ -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 { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
4
5
|
import { isFsApprovalGate, toolNameIsFsWrite, surfaceFsApprovalAndDecide, isToolApprovalFrame, isFromSubagent, surfaceToolApprovalFrameAndRespond, } from './toolApprovalWire.js';
|
|
5
6
|
import { classifierDenyFromToolEnd } from '../classifierVerdictWire.js';
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
19
|
+
hostSurfaceByKey.set(sessionKey, prev);
|
|
14
20
|
};
|
|
15
21
|
}
|
|
16
22
|
/** 🔴 宿主自检:恒应为 0。非 0 = 有 HITL 副作用发生时口不在,那一行 warn / 那条记账丢了。 */
|
|
17
23
|
export function hitlHostSurfaceMisses() {
|
|
18
|
-
return
|
|
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
|
-
|
|
23
|
-
|
|
36
|
+
hostSurfaceByKey.clear();
|
|
37
|
+
hostSurfaceMissesByKey.clear();
|
|
24
38
|
}
|
|
25
39
|
function surface() {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
/**
|
|
@@ -61,10 +61,18 @@ export interface ApprovalCardRequest {
|
|
|
61
61
|
export type ApprovalCardPort = (req: ApprovalCardRequest) => Promise<ApprovalCardDecision>;
|
|
62
62
|
/** 装卡口(传 null 卸)。返回还原函数。 */
|
|
63
63
|
export declare function installApprovalCardPort(port: ApprovalCardPort | null): () => void;
|
|
64
|
+
/** W1 带 key 变体:装/卸某会话的卡口(多会话宿主每会话一键,互不顶盖)。 */
|
|
65
|
+
export declare function installApprovalCardPortFor(sessionKey: string, port: ApprovalCardPort | null): () => void;
|
|
64
66
|
/** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
|
|
65
67
|
export declare function approvalCardPortMisses(): number;
|
|
68
|
+
/** W1 带 key 变体(miss 计数逐键)。 */
|
|
69
|
+
export declare function approvalCardPortMissesFor(sessionKey: string): number;
|
|
66
70
|
/** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
|
|
67
71
|
export declare function hasApprovalCardPort(): boolean;
|
|
72
|
+
/** W1 带 key 变体。 */
|
|
73
|
+
export declare function hasApprovalCardPortFor(sessionKey: string): boolean;
|
|
74
|
+
/** W1 读端:某会话的卡口(未装 = null;**不计 miss** —— miss 只在决断腿真到达时计)。 */
|
|
75
|
+
export declare function approvalCardPortFor(sessionKey: string): ApprovalCardPort | null;
|
|
68
76
|
/** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
|
|
69
77
|
export declare function _resetApprovalCardPortForTest(): void;
|
|
70
78
|
/**
|
|
@@ -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 桥先判)。 */
|
|
@@ -36,28 +37,48 @@ export function structurallyEqual(a, b) {
|
|
|
36
37
|
return false;
|
|
37
38
|
return ka.every(k => structurallyEqual(a[k], b[k]));
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
// W1(design/161):sessionKey → 注册表;零参 API = DEFAULT_SESSION_KEY 兼容层(cli 装配不动)。
|
|
41
|
+
const cardPortByKey = createSessionSlot();
|
|
42
|
+
const cardPortMissesByKey = new Map();
|
|
43
|
+
function bumpCardPortMiss(sessionKey) {
|
|
44
|
+
cardPortMissesByKey.set(sessionKey, (cardPortMissesByKey.get(sessionKey) ?? 0) + 1);
|
|
45
|
+
}
|
|
41
46
|
/** 装卡口(传 null 卸)。返回还原函数。 */
|
|
42
47
|
export function installApprovalCardPort(port) {
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
return installApprovalCardPortFor(DEFAULT_SESSION_KEY, port);
|
|
49
|
+
}
|
|
50
|
+
/** W1 带 key 变体:装/卸某会话的卡口(多会话宿主每会话一键,互不顶盖)。 */
|
|
51
|
+
export function installApprovalCardPortFor(sessionKey, port) {
|
|
52
|
+
const prev = cardPortByKey.get(sessionKey);
|
|
53
|
+
cardPortByKey.set(sessionKey, port);
|
|
45
54
|
return () => {
|
|
46
|
-
|
|
55
|
+
cardPortByKey.set(sessionKey, prev);
|
|
47
56
|
};
|
|
48
57
|
}
|
|
49
58
|
/** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
|
|
50
59
|
export function approvalCardPortMisses() {
|
|
51
|
-
return
|
|
60
|
+
return approvalCardPortMissesFor(DEFAULT_SESSION_KEY);
|
|
61
|
+
}
|
|
62
|
+
/** W1 带 key 变体(miss 计数逐键)。 */
|
|
63
|
+
export function approvalCardPortMissesFor(sessionKey) {
|
|
64
|
+
return cardPortMissesByKey.get(sessionKey) ?? 0;
|
|
52
65
|
}
|
|
53
66
|
/** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
|
|
54
67
|
export function hasApprovalCardPort() {
|
|
55
|
-
return
|
|
68
|
+
return hasApprovalCardPortFor(DEFAULT_SESSION_KEY);
|
|
69
|
+
}
|
|
70
|
+
/** W1 带 key 变体。 */
|
|
71
|
+
export function hasApprovalCardPortFor(sessionKey) {
|
|
72
|
+
return approvalCardPortFor(sessionKey) !== null;
|
|
73
|
+
}
|
|
74
|
+
/** W1 读端:某会话的卡口(未装 = null;**不计 miss** —— miss 只在决断腿真到达时计)。 */
|
|
75
|
+
export function approvalCardPortFor(sessionKey) {
|
|
76
|
+
return cardPortByKey.get(sessionKey) ?? null;
|
|
56
77
|
}
|
|
57
78
|
/** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
|
|
58
79
|
export function _resetApprovalCardPortForTest() {
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
cardPortByKey.clear();
|
|
81
|
+
cardPortMissesByKey.clear();
|
|
61
82
|
}
|
|
62
83
|
/**
|
|
63
84
|
* 共享卡面:弹 vendored CC 三选卡(PermissionRequest)→ 等用户决断。两条腿共用:
|
|
@@ -67,8 +88,9 @@ export function _resetApprovalCardPortForTest() {
|
|
|
67
88
|
* (accept-session 升 intent,[816] 壳侧承诺② —— 与决断通道无关,两腿都要)由**宿主卡口**做。
|
|
68
89
|
*/
|
|
69
90
|
export function surfaceApprovalCard(req) {
|
|
91
|
+
const cardPort = approvalCardPortFor(DEFAULT_SESSION_KEY);
|
|
70
92
|
if (cardPort === null) {
|
|
71
|
-
|
|
93
|
+
bumpCardPortMiss(DEFAULT_SESSION_KEY);
|
|
72
94
|
return Promise.resolve({
|
|
73
95
|
kind: 'failed',
|
|
74
96
|
reason: 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)',
|
|
@@ -85,7 +107,7 @@ export function surfaceApprovalCard(req) {
|
|
|
85
107
|
*/
|
|
86
108
|
export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signal) {
|
|
87
109
|
if (!hasApprovalCardPort()) {
|
|
88
|
-
|
|
110
|
+
bumpCardPortMiss(DEFAULT_SESSION_KEY);
|
|
89
111
|
return { kind: 'failed', reason: 'no approval card port installed (print/non-REPL mode)' };
|
|
90
112
|
}
|
|
91
113
|
// pending 行 = gate 的权威源 + D-1 绑定 surface(与 ask 桥同姿势)。
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
46
|
+
slot.state.log = ports.log;
|
|
27
47
|
if ('probe' in ports)
|
|
28
|
-
|
|
48
|
+
slot.state.probe = ports.probe;
|
|
29
49
|
if ('timers' in ports)
|
|
30
|
-
|
|
50
|
+
slot.state.timers = ports.timers;
|
|
31
51
|
if ('settings' in ports)
|
|
32
|
-
|
|
52
|
+
slot.state.settings = ports.settings;
|
|
33
53
|
if ('fs' in ports)
|
|
34
|
-
|
|
54
|
+
slot.state.fs = ports.fs;
|
|
35
55
|
if ('session' in ports)
|
|
36
|
-
|
|
37
|
-
if ('queue' in ports)
|
|
38
|
-
installNotificationQueuePort(ports.queue ?? null);
|
|
56
|
+
slot.state.session = ports.session;
|
|
39
57
|
return () => {
|
|
40
|
-
|
|
58
|
+
slot.state = prev;
|
|
41
59
|
};
|
|
42
60
|
}
|
|
43
61
|
/** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
|
|
44
62
|
export function hostLog(level, msg, data) {
|
|
45
|
-
|
|
63
|
+
hostSlot(DEFAULT_SESSION_KEY).state.log?.(level, msg, data);
|
|
46
64
|
}
|
|
47
65
|
/** 行式探针(缺席 = 静默,不计 miss:同上)。 */
|
|
48
66
|
export function hostProbe(channel, line) {
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -3,43 +3,10 @@
|
|
|
3
3
|
* 本模块自持 AskUserQuestion 的 wire 词汇(SDK 0.0.44 删掉了 questions 资源导出),搬入后这套
|
|
4
4
|
* 类型随包发布 —— web/桌面端的 HITL 覆盖层与 TUI 共用同一份 QuestionFrame/QuestionAnswer 形。
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* WHY THIS EXISTS
|
|
11
|
-
* ---------------
|
|
12
|
-
* In TOC (two-process) the MODEL runs in the engine; its `AskUserQuestion` tool fires on core's
|
|
13
|
-
* `RunnerDeps.onQuestion` seam and the service (question.ts, 1.61.0) emits a NAMED SSE frame
|
|
14
|
-
* (`event: question` / `event: question_complete`) that INTERLEAVES on the same live token stream as the
|
|
15
|
-
* AgentEvents. That frame is NOT an AgentEvent arm (it is a DISTINCT vocabulary, exactly like elicitation) —
|
|
16
|
-
* so it must be DEMUXED out of the transcript-render pipeline and routed to an interactive DIALOG overlay
|
|
17
|
-
* instead. This tiny, dependency-FREE module is that route: liveClient WRITES the demuxed frame here (on each
|
|
18
|
-
* `question`/`question_complete`) and publishes its `client.questions.respond` binding; the UI overlay
|
|
19
|
-
* (main.tsx / REPL) READS the frames and answers via `respondToQuestion`. Same shape as liveSessionStore /
|
|
20
|
-
* rewindAnchorStore — the overlay stays testable (no @sema-ai/sdk graph) and seamQuery/normalizeToolNames are
|
|
21
|
-
* untouched.
|
|
22
|
-
*
|
|
23
|
-
* 🔴 LIVE-ONLY + SAME-REPLICA (the `steer()`-class of HITL): the service parks the answer promise in-memory,
|
|
24
|
-
* so a respond that lands after the TTL (5 min) / after answer / on another replica → 404. No durable resume
|
|
25
|
-
* anchor — a mid-question disconnect just lets the run proceed with the headless default (the model never
|
|
26
|
-
* hangs). The overlay must therefore treat `respondToQuestion` as best-effort (a 404 = "already released",
|
|
27
|
-
* dismiss the dialog, never retry).
|
|
28
|
-
*
|
|
29
|
-
* 🔴 UNTRUSTED: the `questions` payload is model-authored (service secret-redacted, but still UNTRUSTED for
|
|
30
|
-
* display). The overlay RENDERS it; it must NEVER be re-fed to a model. The answer the overlay sends back is
|
|
31
|
-
* fenced by CORE (`selected ⊆ options`, `note` untrusted-fenced) — the overlay must echo the EXACT option
|
|
32
|
-
* labels the user saw, never fabricate one.
|
|
33
|
-
*/
|
|
34
|
-
/* ── LOCAL wire types (owned HERE, not @sema-ai/sdk) ─────────────────────────────────────────────
|
|
35
|
-
* SDK 0.0.44 DELETED the `questions` resource (engine 1.163 parks the run `suspended` instead of
|
|
36
|
-
* emitting `question` SSE frames — see liveHitlAskWire.ts), and with it the QuestionFrame /
|
|
37
|
-
* QuestionAnswer / AskQuestion exports. The shell still speaks this vocabulary INTERNALLY (synthetic
|
|
38
|
-
* frames from liveHitlAskWire / planReviewWire + the legacy 1.61-era SSE demux in liveClient), so the
|
|
39
|
-
* types now live in THIS dependency-free module — the store this whole seam already routes through.
|
|
40
|
-
* Shapes are the ones the shell actually produces/consumes (askq-wire-probe + overlay usage), byte-
|
|
41
|
-
* compatible with the old wire payload `{type,questionId,questions?,outcome?}`. */
|
|
42
|
-
let frameHandler = null;
|
|
6
|
+
import { createSessionSlot, DEFAULT_SESSION_KEY } from './sessionSlot.js';
|
|
7
|
+
// W1(design/161):frameHandler 槽位改 sessionKey → 注册表;零参 API = DEFAULT_SESSION_KEY
|
|
8
|
+
// 兼容层(cli 装配不动)。respondFn / localResponders 不在 W1 五槽位切口内,维持原样。
|
|
9
|
+
const frameHandlerByKey = createSessionSlot();
|
|
43
10
|
let respondFn = null;
|
|
44
11
|
/** SHELL-LOCAL question responders (plan-review approval cards etc. — synthetic frames whose id does
|
|
45
12
|
* NOT exist engine-side, so POST /v1/questions/:id/respond would 404). Registered per-questionId by
|
|
@@ -59,6 +26,11 @@ export function registerLocalQuestionResponder(id, fn) {
|
|
|
59
26
|
* break the live stream drain (the run must keep flowing; a lost frame just headless-defaults the ask).
|
|
60
27
|
*/
|
|
61
28
|
export function publishQuestionFrame(frame) {
|
|
29
|
+
publishQuestionFrameFor(DEFAULT_SESSION_KEY, frame);
|
|
30
|
+
}
|
|
31
|
+
/** W1 keyed variant: publish to ONE session's overlay (multi-session hosts demux by their own key). */
|
|
32
|
+
export function publishQuestionFrameFor(sessionKey, frame) {
|
|
33
|
+
const frameHandler = frameHandlerByKey.get(sessionKey);
|
|
62
34
|
if (!frameHandler)
|
|
63
35
|
return;
|
|
64
36
|
try {
|
|
@@ -71,14 +43,23 @@ export function publishQuestionFrame(frame) {
|
|
|
71
43
|
/** Whether a UI overlay is currently subscribed (liveHitlAskWire gates on this: print/non-REPL mode has no
|
|
72
44
|
* dialog to park on, so the suspended terminal must fall through honestly instead of hanging the stream). */
|
|
73
45
|
export function hasQuestionOverlay() {
|
|
74
|
-
return
|
|
46
|
+
return hasQuestionOverlayFor(DEFAULT_SESSION_KEY);
|
|
47
|
+
}
|
|
48
|
+
/** W1 keyed variant. */
|
|
49
|
+
export function hasQuestionOverlayFor(sessionKey) {
|
|
50
|
+
return frameHandlerByKey.get(sessionKey) !== undefined;
|
|
75
51
|
}
|
|
76
52
|
/** Subscribe the UI overlay to live question frames. Returns an unsubscribe. Last writer wins (one overlay). */
|
|
77
53
|
export function onQuestionFrame(handler) {
|
|
78
|
-
|
|
54
|
+
return onQuestionFrameFor(DEFAULT_SESSION_KEY, handler);
|
|
55
|
+
}
|
|
56
|
+
/** W1 keyed variant: one overlay PER sessionKey (last writer wins within a key; keys never clobber
|
|
57
|
+
* each other — the r1 E2 dual-session hazard this registry exists to close). */
|
|
58
|
+
export function onQuestionFrameFor(sessionKey, handler) {
|
|
59
|
+
frameHandlerByKey.set(sessionKey, handler);
|
|
79
60
|
return () => {
|
|
80
|
-
if (
|
|
81
|
-
|
|
61
|
+
if (frameHandlerByKey.get(sessionKey) === handler)
|
|
62
|
+
frameHandlerByKey.set(sessionKey, undefined);
|
|
82
63
|
};
|
|
83
64
|
}
|
|
84
65
|
/** Publish the live client's `questions.respond` binding (called from createLiveConversationClient). `null`
|
|
@@ -104,6 +85,6 @@ export function respondToQuestion(id, answer, opts) {
|
|
|
104
85
|
}
|
|
105
86
|
/** Test-only reset. */
|
|
106
87
|
export function _resetLiveQuestionStore() {
|
|
107
|
-
|
|
88
|
+
frameHandlerByKey.clear();
|
|
108
89
|
respondFn = null;
|
|
109
90
|
}
|
|
@@ -81,6 +81,8 @@ export interface TaskRequestInput {
|
|
|
81
81
|
attachments?: Record<string, unknown>;
|
|
82
82
|
/** 预上传附件句柄(`uploadAttachment` 回执 id)。空数组 = 没带附件 ⇒ 整键不 stamp。 */
|
|
83
83
|
attachmentIds?: readonly string[];
|
|
84
|
+
/** 排除工具名单(SDK 真 wire 键;desktop 座位面在发)。空数组 = 不排除 ⇒ 整键不 stamp。 */
|
|
85
|
+
excludeTools?: readonly string[];
|
|
84
86
|
finalVerification?: boolean;
|
|
85
87
|
limits?: Record<string, unknown>;
|
|
86
88
|
interactiveTools?: false;
|
|
@@ -34,6 +34,7 @@ export const REQUEST_FIELD_MATRIX = [
|
|
|
34
34
|
// B4 从壳的三个构造器逐行读出来的,那时这条车道只有壳一个消费者。`attachmentIds` 是**反过来**
|
|
35
35
|
// 的第一条:它今天只有 web/desktop 在发,壳反而没有这个入口。别把本节读成「壳独有」。
|
|
36
36
|
{ field: 'attachmentIds', lanes: ['interactive'], live: true, why: '附件**字节通道**(uploadAttachment 回执 id;server ≥1.289 绑定会话并把文件物化进 run 的 attachments/)。有上传入口的端才有 id 可引用:web/desktop 有,壳走 images / 本地文件路径,`-p` 车道连 uploadAttachment 都没有 ⇒ print 缺席是**没有来源**,不是漏。🔴 与上面的 `attachments` 同名不同物(那是 turn 边界的配置键),两条永远不许合并' },
|
|
37
|
+
{ field: 'excludeTools', lanes: ['interactive'], live: true, why: 'design/161 W3(2026-07-30):真 wire 键(SDK types `excludeTools`)且早在 seatContract `START_SESSION_OPTION_KEYS` 里,却是 desktop 手写 taskReq 的第 9 键、矩阵外——而 stamp 门对未登记键**静默丢弃**,真实危险形=「用户显式排除的工具被静默放回」(权限方向回归,类型层不报)。今天只有 desktop 在发;壳的工具面走 roster/interactiveTools 另一条路,print 无排除入口' },
|
|
37
38
|
{ field: 'settings.ultracode', lanes: ['interactive'], live: true, why: 'design/111:sticky `/effort ultracode` 拨盘 + 当轮关键词嗅探,两个来源都只在交互面存在', gap: true },
|
|
38
39
|
{ field: 'systemPrompt', lanes: ['interactive'], live: false, why: 'CC QueryParams.systemPrompt;print 腿的 params 没有这一位' },
|
|
39
40
|
{ field: 'reasoningEffort', lanes: ['interactive'], live: false, why: '`/effort` 拨盘存在 AppState,print 无 AppState', gap: true },
|
|
@@ -115,6 +116,10 @@ export function buildTaskRequest(input, lane) {
|
|
|
115
116
|
...(on('attachmentIds', input.attachmentIds) && (input.attachmentIds?.length ?? 0) > 0
|
|
116
117
|
? { attachmentIds: [...(input.attachmentIds ?? [])] }
|
|
117
118
|
: {}),
|
|
119
|
+
// 同款空数组语义:「不排除任何工具」不 stamp(缺席与空排除在 wire 上等价,发小的那个)。
|
|
120
|
+
...(on('excludeTools', input.excludeTools) && (input.excludeTools?.length ?? 0) > 0
|
|
121
|
+
? { excludeTools: [...(input.excludeTools ?? [])] }
|
|
122
|
+
: {}),
|
|
118
123
|
...(on('clientContext', input.clientContext) ? { clientContext: input.clientContext } : {}),
|
|
119
124
|
...(on('scratchpadDir', input.scratchpadDir) ? { scratchpadDir: input.scratchpadDir } : {}),
|
|
120
125
|
...(on('finalVerification', input.finalVerification)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessionSlot.ts — W1(design/161):包内「模块级可变单槽位」的 per-session 注册表最小工厂。
|
|
3
|
+
*
|
|
4
|
+
* 背景(design/161 复审 r1 E2):包内五个模块级单槽位(host.ts `host` / askGateWire
|
|
5
|
+
* `hostSurface` / toolApprovalWire `cardPort` / liveQuestionStore `frameHandler` /
|
|
6
|
+
* engineWireTarget `installed`)与多会话宿主(desktop)正面冲突 —— 两个并行会话互相顶盖
|
|
7
|
+
* (A 会话的审批卡弹到 B 的面上 / B 的 responder 覆盖 A 的)。W1 把这五个槽位改成
|
|
8
|
+
* `Map<sessionKey, T>` 注册表:
|
|
9
|
+
* · **零参旧 API = DEFAULT_SESSION_KEY 兼容层**(cli 的 module-load 装配一行不动,
|
|
10
|
+
* default 键路径与改前单变量行为逐字节等价);
|
|
11
|
+
* · 带 key 的 `*For(sessionKey, …)` 变体给多会话宿主用,每会话一键,互不顶盖。
|
|
12
|
+
* 契约锚 = `agentSession/contract.ts` 的 `AgentSessionConfig.sessionKey`。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 本文件只管「存取」,不带任何业务语义 —— miss 计数、fail-soft 文言、还原/合并语义都留在
|
|
15
|
+
* 各槽位模块里(五个槽位形状略异,强行归一就是过度设计)。
|
|
16
|
+
*/
|
|
17
|
+
/** 零参旧 API 的兼容键(cli 单会话装配走它;多会话宿主每会话一键,别用这个值当会话 id)。 */
|
|
18
|
+
export declare const DEFAULT_SESSION_KEY = "__default__";
|
|
19
|
+
/** per-session 槽位存取面。`get` 未装过 = undefined(各槽位模块自己 `?? null` 归一)。 */
|
|
20
|
+
export interface SessionSlot<T> {
|
|
21
|
+
get(key: string): T | undefined;
|
|
22
|
+
/** `undefined` = 卸下该键(与「从未装过」同态 —— 槽位模块的 prev-还原语义靠它闭合)。 */
|
|
23
|
+
set(key: string, value: T | undefined): void;
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function createSessionSlot<T>(): SessionSlot<T>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessionSlot.ts — W1(design/161):包内「模块级可变单槽位」的 per-session 注册表最小工厂。
|
|
3
|
+
*
|
|
4
|
+
* 背景(design/161 复审 r1 E2):包内五个模块级单槽位(host.ts `host` / askGateWire
|
|
5
|
+
* `hostSurface` / toolApprovalWire `cardPort` / liveQuestionStore `frameHandler` /
|
|
6
|
+
* engineWireTarget `installed`)与多会话宿主(desktop)正面冲突 —— 两个并行会话互相顶盖
|
|
7
|
+
* (A 会话的审批卡弹到 B 的面上 / B 的 responder 覆盖 A 的)。W1 把这五个槽位改成
|
|
8
|
+
* `Map<sessionKey, T>` 注册表:
|
|
9
|
+
* · **零参旧 API = DEFAULT_SESSION_KEY 兼容层**(cli 的 module-load 装配一行不动,
|
|
10
|
+
* default 键路径与改前单变量行为逐字节等价);
|
|
11
|
+
* · 带 key 的 `*For(sessionKey, …)` 变体给多会话宿主用,每会话一键,互不顶盖。
|
|
12
|
+
* 契约锚 = `agentSession/contract.ts` 的 `AgentSessionConfig.sessionKey`。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 本文件只管「存取」,不带任何业务语义 —— miss 计数、fail-soft 文言、还原/合并语义都留在
|
|
15
|
+
* 各槽位模块里(五个槽位形状略异,强行归一就是过度设计)。
|
|
16
|
+
*/
|
|
17
|
+
/** 零参旧 API 的兼容键(cli 单会话装配走它;多会话宿主每会话一键,别用这个值当会话 id)。 */
|
|
18
|
+
export const DEFAULT_SESSION_KEY = '__default__';
|
|
19
|
+
export function createSessionSlot() {
|
|
20
|
+
const values = new Map();
|
|
21
|
+
return {
|
|
22
|
+
get: (key) => values.get(key),
|
|
23
|
+
set: (key, value) => {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
values.delete(key);
|
|
26
|
+
else
|
|
27
|
+
values.set(key, value);
|
|
28
|
+
},
|
|
29
|
+
clear: () => values.clear(),
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/workflowClient.d.ts
CHANGED
|
@@ -15,6 +15,26 @@ export interface LiveWorkflowController {
|
|
|
15
15
|
runId: string | null;
|
|
16
16
|
lastError: string | null;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to "the projected snapshot changed" — fired on the FIRST successful `get()` and on every
|
|
20
|
+
* subsequent re-project. Returns an unsubscribe.
|
|
21
|
+
*
|
|
22
|
+
* WHY (clay 2026-07-30 症状②「入场蹲卡」): every host polls `snapshot()` on its own 1s interval, so a
|
|
23
|
+
* `get()` that answered in 50ms still took up to 1s to reach the screen — a purely self-inflicted delay,
|
|
24
|
+
* and during that window the panel renders its not-yet-resolved fill. The interval stays (it also drives
|
|
25
|
+
* the wall-clock cells: idle/duration/waiting), but the FIRST resolve no longer waits for a tick.
|
|
26
|
+
*
|
|
27
|
+
* 🔴 SUBSCRIBE-AND-RECONCILE: if a snapshot has ALREADY resolved by the time you subscribe, the callback fires
|
|
28
|
+
* ONCE synchronously from `subscribe` itself. Without this the notify arm and the subscribe arm do not share a
|
|
29
|
+
* premise — a host that renders (snapshot null) → subscribes in an effect loses the notify entirely whenever
|
|
30
|
+
* `get()` won that race, and falls back to waiting a full poll tick. That is exactly the residual outlier the
|
|
31
|
+
* entry-latency fence caught (2 of 3 runs ≈200-340ms, 1 run ≈1026ms = one whole tick).
|
|
32
|
+
*
|
|
33
|
+
* Contract: the callback runs INSIDE the source's own loop (or synchronously from `subscribe`) — hosts must only
|
|
34
|
+
* schedule a re-render from it, never re-enter the controller synchronously. A throwing callback is swallowed so
|
|
35
|
+
* one bad subscriber cannot kill the background consumer.
|
|
36
|
+
*/
|
|
37
|
+
subscribe(onChange: () => void): () => void;
|
|
18
38
|
/** abort the underlying SSE stream + stop the background loop (host unmount). */
|
|
19
39
|
dispose(): void;
|
|
20
40
|
}
|