@sema-agent/client-core 0.86.0 → 0.88.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 (57) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +18 -12
  3. package/dist/adapt/arms.js +9 -0
  4. package/dist/adapter/downstream/eventToSdkMessage.js +14 -1
  5. package/dist/adapter/downstream/terminalToSdkResult.js +4 -1
  6. package/dist/adapter/downstream/wiringManifestView.d.ts +2 -0
  7. package/dist/adapter/downstream/wiringManifestView.js +1 -0
  8. package/dist/adapter/runStream.js +75 -2
  9. package/dist/adapter/types.d.ts +3 -0
  10. package/dist/decideReceipt.d.ts +1 -1
  11. package/dist/decideReceipt.js +1 -1
  12. package/dist/detachWire.d.ts +6 -1
  13. package/dist/detachWire.js +22 -1
  14. package/dist/displayUntrusted.d.ts +4 -0
  15. package/dist/displayUntrusted.js +1657 -573
  16. package/dist/engineErrorCodes.d.ts +2 -0
  17. package/dist/engineErrorCodes.js +1 -0
  18. package/dist/engineNoticeCodes.d.ts +20 -0
  19. package/dist/engineNoticeCodes.js +71 -0
  20. package/dist/engineWireFor.d.ts +1 -0
  21. package/dist/engineWireFor.js +1 -1
  22. package/dist/gateOutcome.d.ts +4 -0
  23. package/dist/gateOutcome.js +5 -0
  24. package/dist/generated/engineFactTables.d.ts +1 -0
  25. package/dist/generated/engineFactTables.js +3 -0
  26. package/dist/generated/engineNoticeTables.js +2 -0
  27. package/dist/hitl/crashConverged.d.ts +2 -1
  28. package/dist/hitl/crashConverged.js +23 -3
  29. package/dist/hitl/hitlBridge.d.ts +4 -0
  30. package/dist/hitl/hitlBridge.js +6 -0
  31. package/dist/hitl/parkResolver.js +17 -12
  32. package/dist/hitl/planReviewWire.js +30 -10
  33. package/dist/hitl/suspendedReopen.d.ts +7 -0
  34. package/dist/hitl/suspendedReopen.js +38 -7
  35. package/dist/hitl/toolApprovalWire.js +4 -5
  36. package/dist/index.d.ts +7 -0
  37. package/dist/index.js +4 -0
  38. package/dist/promptDelivery.d.ts +20 -0
  39. package/dist/promptDelivery.js +153 -0
  40. package/dist/registryConflict.d.ts +3 -0
  41. package/dist/registryConflict.js +44 -0
  42. package/dist/request/taskRequest.d.ts +1 -0
  43. package/dist/request/taskRequest.js +25 -1
  44. package/dist/resumeRefusalCopy.d.ts +8 -6
  45. package/dist/resumeRefusalCopy.js +41 -19
  46. package/dist/rewindArchiveCapability.js +5 -0
  47. package/dist/seam.d.ts +12 -1
  48. package/dist/seam.js +7 -0
  49. package/dist/sessionMemoryErase.d.ts +76 -0
  50. package/dist/sessionMemoryErase.js +432 -0
  51. package/dist/subagent/engineSubagentResume.js +2 -1
  52. package/dist/toolHistoryMismatch.d.ts +63 -0
  53. package/dist/toolHistoryMismatch.js +425 -0
  54. package/dist/workflowClient.d.ts +3 -2
  55. package/dist/workflowClient.js +3 -3
  56. package/docs/INTEGRATION-CLIENTS.md +598 -13
  57. package/package.json +2 -2
@@ -0,0 +1,153 @@
1
+ import { activeRunBusySignal } from './adapter/runStream.js';
2
+ import { selfHealSubmissionDisposition } from './adapter/activeRunSelfHeal.js';
3
+ const uuidOk = (u) => typeof u === 'string' && u.length > 0;
4
+ function cleanBatch(batch) {
5
+ if (!Array.isArray(batch))
6
+ return [];
7
+ const out = [];
8
+ const seen = new Set();
9
+ try {
10
+ for (const u of batch) {
11
+ if (!uuidOk(u) || seen.has(u))
12
+ continue;
13
+ seen.add(u);
14
+ out.push(u);
15
+ }
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ return out;
21
+ }
22
+ function readRecord(rec) {
23
+ if (typeof rec !== 'object' || rec === null || Array.isArray(rec))
24
+ return undefined;
25
+ try {
26
+ const u = rec.u;
27
+ const s = rec.s;
28
+ return uuidOk(u) && (s === 'a' || s === 'd') ? { u, s } : undefined;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
34
+ const PRE_RUN_FRAME_TYPES = new Set(['meta', 'approval_request', 'approval_revoke']);
35
+ function frameProvesDelivery(event) {
36
+ if (typeof event !== 'object' || event === null || Array.isArray(event))
37
+ return false;
38
+ try {
39
+ const type = event.type;
40
+ if (typeof type !== 'string' || type.length === 0 || PRE_RUN_FRAME_TYPES.has(type))
41
+ return false;
42
+ if (type === 'done') {
43
+ const result = event.result;
44
+ if (typeof result === 'object' && result !== null && !Array.isArray(result) && result.status === undefined) {
45
+ const r = result;
46
+ if (activeRunBusySignal({ type: 'failed', errorCode: r.errorCode, activeTaskId: r.activeTaskId }) !== null)
47
+ return false;
48
+ }
49
+ }
50
+ return activeRunBusySignal(event) === null;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function outcomeProvesDelivery(outcome) {
57
+ if (typeof outcome !== 'object' || outcome === null || Array.isArray(outcome))
58
+ return false;
59
+ try {
60
+ return selfHealSubmissionDisposition(outcome) === 'handed-off';
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ export function createPromptDeliveryLedger() {
67
+ const attemptedSet = new Set();
68
+ const deliveredSet = new Set();
69
+ const batchOfMap = new Map();
70
+ const log = [];
71
+ const add = (u, s, into) => {
72
+ const set = s === 'a' ? attemptedSet : deliveredSet;
73
+ if (set.has(u))
74
+ return;
75
+ set.add(u);
76
+ const rec = Object.freeze({ u, s });
77
+ log.push(rec);
78
+ into.push(rec);
79
+ };
80
+ const markDelivered = (batchUuids) => {
81
+ const added = [];
82
+ for (const u of cleanBatch(batchUuids))
83
+ add(u, 'd', added);
84
+ return Object.freeze(added);
85
+ };
86
+ return Object.freeze({
87
+ attempted(batchUuids) {
88
+ const batch = Object.freeze(cleanBatch(batchUuids));
89
+ const added = [];
90
+ for (const u of batch) {
91
+ batchOfMap.set(u, batch);
92
+ add(u, 'a', added);
93
+ }
94
+ return Object.freeze(added);
95
+ },
96
+ deliveredByFrame(batchUuids, event) {
97
+ return frameProvesDelivery(event) ? markDelivered(batchUuids) : Object.freeze([]);
98
+ },
99
+ deliveredBySteerReceipt(batchUuids, outcome) {
100
+ return outcomeProvesDelivery(outcome) ? markDelivered(batchUuids) : Object.freeze([]);
101
+ },
102
+ isAttempted: (uuid) => uuidOk(uuid) && attemptedSet.has(uuid),
103
+ isDelivered: (uuid) => uuidOk(uuid) && deliveredSet.has(uuid),
104
+ knownUndelivered: (uuid) => uuidOk(uuid) && attemptedSet.has(uuid) && !deliveredSet.has(uuid),
105
+ batchOf: (uuid) => (uuidOk(uuid) ? batchOfMap.get(uuid) : undefined),
106
+ toRecords: () => Object.freeze([...log]),
107
+ fromRecords(records) {
108
+ const added = [];
109
+ if (!Array.isArray(records))
110
+ return Object.freeze(added);
111
+ let list;
112
+ try {
113
+ list = [...records];
114
+ }
115
+ catch {
116
+ return Object.freeze(added);
117
+ }
118
+ for (const raw of list) {
119
+ const rec = readRecord(raw);
120
+ if (rec !== undefined)
121
+ add(rec.u, rec.s, added);
122
+ }
123
+ return Object.freeze(added);
124
+ },
125
+ });
126
+ }
127
+ function uuidOfMessage(m) {
128
+ if (typeof m !== 'object' || m === null)
129
+ return undefined;
130
+ try {
131
+ const u = m.uuid;
132
+ return uuidOk(u) ? u : undefined;
133
+ }
134
+ catch {
135
+ return undefined;
136
+ }
137
+ }
138
+ function assertLedger(ledger, fn) {
139
+ if (typeof ledger !== 'object' || ledger === null || typeof ledger.knownUndelivered !== 'function') {
140
+ throw new TypeError(`${fn}: the first argument must be a ledger from createPromptDeliveryLedger()`);
141
+ }
142
+ }
143
+ export function promptKnownUndelivered(ledger, message) {
144
+ assertLedger(ledger, 'promptKnownUndelivered');
145
+ const u = uuidOfMessage(message);
146
+ return u !== undefined && ledger.knownUndelivered(u);
147
+ }
148
+ export function withoutUndeliveredPrompts(ledger, messages) {
149
+ assertLedger(ledger, 'withoutUndeliveredPrompts');
150
+ if (!Array.isArray(messages))
151
+ return [];
152
+ return messages.filter((m) => !promptKnownUndelivered(ledger, m));
153
+ }
@@ -0,0 +1,3 @@
1
+ export type RegistryConflictClass = 'gone' | 'conflict' | 'none';
2
+ export declare function classifyRegistryConflict(status: number, body: unknown): RegistryConflictClass;
3
+ export declare function isScopeGoneError(err: unknown): boolean;
@@ -0,0 +1,44 @@
1
+ const SCOPE_GONE_CODE = 'scope_gone';
2
+ const nonBlank = (v) => {
3
+ const s = typeof v === 'string' ? v.trim() : '';
4
+ return s === '' ? undefined : s;
5
+ };
6
+ export function classifyRegistryConflict(status, body) {
7
+ if (status !== 409)
8
+ return 'none';
9
+ try {
10
+ const b = (typeof body === 'object' && body !== null ? body : {});
11
+ const error = b.error;
12
+ const objectForm = typeof error === 'object' && error !== null ? error.code : undefined;
13
+ const code = nonBlank(b.errorCode) ?? nonBlank(objectForm) ?? (typeof error === 'string' ? error : undefined);
14
+ return code === SCOPE_GONE_CODE ? 'gone' : 'conflict';
15
+ }
16
+ catch {
17
+ return 'gone';
18
+ }
19
+ }
20
+ const SCOPE_GONE_ERROR_NAMES = Object.freeze(['ScopeGoneError', 'ControlScopeGoneError']);
21
+ export function isScopeGoneError(err) {
22
+ if ((typeof err !== 'object' && typeof err !== 'function') || err === null)
23
+ return false;
24
+ let status;
25
+ try {
26
+ status = err.status;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ if (status !== 409)
32
+ return false;
33
+ try {
34
+ const o = err;
35
+ const body = o.body;
36
+ if (body !== undefined && body !== null && classifyRegistryConflict(409, body) === 'gone')
37
+ return true;
38
+ const name = o.name;
39
+ return typeof name === 'string' && SCOPE_GONE_ERROR_NAMES.includes(name);
40
+ }
41
+ catch {
42
+ return true;
43
+ }
44
+ }
@@ -50,6 +50,7 @@ export interface TaskRequestInput {
50
50
  memory?: 'off';
51
51
  fileHistory?: 'disabled';
52
52
  inheritEnv?: InheritEnvWord;
53
+ requireExistingSession?: TaskRequest['requireExistingSession'];
53
54
  rewind?: Record<string, unknown>;
54
55
  repo?: string;
55
56
  council?: boolean;
@@ -24,6 +24,7 @@ export const REQUEST_FIELD_MATRIX = [
24
24
  { field: 'memory', lanes: ['interactive', 'print', 'utility'], live: true, why: '这一次运行不挂记忆面的**请求级**开关(server §12.7 body 键,单成员闭集 "off");每条车道都是提交面 ⇒ 都能声明。与 memoryCapture(采集面)不是一个轴、可并存;与 memoryWrite:false(只读平面)也不是一个轴', whyUtilityLane: '有座:side-channel 提交不需要挂记忆面(不挂记忆段 / 索引 / 三只记忆工具),省 token 且零记忆写入;与 memoryCapture 不是一个轴,两条都带' },
25
25
  { field: 'fileHistory', lanes: ['interactive', 'print'], live: true, why: '「这一次运行别捕获文件历史」的**请求级声明**(server 7.104.0 提交体键,单成员闭集 "disabled";`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs` 同收,子代继承)。两条用户车道都是提交面、都跑在用户的文件上 ⇒ 都能声明;「本会话不捕获」= 每个提交都带(续跑腿重放同一个体)。🔴 只在能力读数 `on-optional` 时发(老 server 对表外键 400),值由片段口 `fileHistoryDeclarationField` 唯一铸出。与 rewind 还原键同交 ⇒ 上游 400 `request.field_conflict`', whyUtilityLane: '无座:side-channel 强制零工具面(`excludeAllTools`),不会改任何文件 ⇒ 没有可捕获的东西;带上它只会在不认识这一键的老 server 上给这条提交多一种 400' },
26
26
  { field: 'inheritEnv', lanes: ['interactive', 'print'], live: true, why: '模型驱动的宿主 shell「继承多少用户环境」的**请求级声明**(server 7.106.0 提交体键,两词闭集 "all" / "scrub";`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs` 同收,续跑腿重放同判)。两条用户车道都是提交面、都可能在用户机器上跑 shell ⇒ 都能声明。本层两词原样透传、不按车道 / 部署预判(非 host 车道 × "all" 400、托管 × "all" 422 归上游判)。🔴 缺席 = 缺省清洗且清掉本会话上一条声明 ⇒ 要「本会话整份继承」就每个提交都带。🔴 只在服务端版本 ≥ 7.106.0 时发(老 server 对表外键整条 400),值由片段口 `inheritEnvDeclaration` 唯一铸出', whyUtilityLane: '无座:side-channel 强制零工具面(`excludeAllTools`),没有模型驱动的 shell 去继承环境;带上它只会在不认这一键的老服务端上给这条提交多一种 400' },
27
+ { field: 'requireExistingSession', lanes: ['interactive', 'print'], live: true, why: '「只续已有会话」的**请求级断言**(sdk `TaskRequest.requireExistingSession`):`true` 且会话不在 ⇒ 服务端在任何东西跑起来之前拒(会话不在 / 没给会话 id 两形),而**不是**静默新开一个同 id 的空会话;缺席 / `false` = 历史的「找不到就新开」。两条用户车道都有「续某个会话」的入口 ⇒ 都能声明。本层只认字面 `true`(`false` 与缺席同义、不发);非布尔坏值响亮拒(上游对它 400);`true` 而入参没有可续的会话 id ⇒ 装配期按名响亮拒,不留给上游。上游两形的拒绝经 `requireExistingSessionRefusalFromError` 归成一句', whyUtilityLane: '无座:side-channel 每一次提交都走壳另铸的**独立**会话 id(见 sessionId 那一行),从不续任何已有会话 ⇒「必须已存在」对它没有意义,带上去只会让每一次提交都被拒' },
27
28
  { field: 'finalVerification', lanes: ['print'], live: true, why: 'P1-1 终验:无人值守车道才需要引擎自证;交互 REPL 由人当场看结果。#106 裁 B(让位+告知)后交互面默认关', whyUtilityLane: '无座:终验让引擎再跑一轮自证,而 side-channel 要的是一次小提交的即时答案;「无人值守才需要引擎自证」这条理由在本车道上让位给「小预算优先」' },
28
29
  { field: 'limits', lanes: ['print', 'utility'], live: true, why: 'P2-3-b:`-p` 的预算护栏(--max-* flag 族),交互 REPL 由人随时 Esc', whyUtilityLane: '有座:side-channel 必须带预算护栏(墙钟 / 输出 token / 轮数)—— 它同样没有人看着,而且没有后续回合可以中断' },
29
30
  { field: 'interactiveTools', lanes: ['print', 'utility'], live: true, why: '[909]B 件3:无人值守 stamp false,从 roster 源头灭掉 AskUserQuestion/plan 门。交互车道 stamp false 等于自废武功', whyUtilityLane: '有座:side-channel 绝不该弹交互问答 / plan 门(没有人在看这条提交)。它与 excludeAllTools 是**两条独立**的收紧声明、在这条车道上**并存**(本车道强制带卸载声明,见 `refuseUtilityWithoutToolUnload`;这一位是它旁边那条「连问都别问」的声明,不是它的降级替身)' },
@@ -139,6 +140,7 @@ const INPUT_KEY_ROUTES = {
139
140
  memory: row('memory'),
140
141
  fileHistory: row('fileHistory'),
141
142
  inheritEnv: row('inheritEnv'),
143
+ requireExistingSession: row('requireExistingSession'),
142
144
  rewind: row('resumeAt/resumeAtMode/restoreFiles/rewindFilesTo'),
143
145
  repo: row('repo'),
144
146
  council: row('council'),
@@ -211,6 +213,7 @@ const VALUE_LEVEL_IN_PLAY = Object.freeze({
211
213
  memoryCapture: (v) => v === 'off',
212
214
  memory: (v) => v === 'off',
213
215
  fileHistory: (v) => v === FILE_HISTORY_DISABLED,
216
+ requireExistingSession: (v) => v === true,
214
217
  rewind: (v) => typeof v === 'object' && v !== null && rewindSubKeys().some((k) => hasOwn(v, k) && present(v[k])),
215
218
  });
216
219
  const inPlay = (key, v) => present(v) && (VALUE_LEVEL_IN_PLAY[key]?.(v) ?? true);
@@ -454,6 +457,22 @@ const refuseBadInheritEnv = (v) => {
454
457
  '这一位是「模型驱动的宿主 shell 继承多少用户环境」的两词闭集声明,上游对词表外的值响亮拒;环境变量名单不是请求值。' +
455
458
  '本层若静默删键,这一次运行会按缺省清洗跑、还清掉本会话上一条声明,而声明人不会知道。错误文本只报形状,不回显值。');
456
459
  };
460
+ const refuseBadRequireExistingSession = (v) => {
461
+ if (v === undefined || v === null || v === true || v === false)
462
+ return;
463
+ throw new TypeError(`buildTaskRequest: requireExistingSession 只接受布尔或缺席(undefined/null)—— 收到 ${coarseShape(v)}。` +
464
+ '这一位是「只续已有会话、找不到就失败」的收紧断言:本层若把坏值折成缺席,这一次提交在会话不在时会静默新开一个空会话 —— 正是这一键要挡的那一形;' +
465
+ '上游对非布尔值同样拒收。错误文本只报形状,不回显值。');
466
+ };
467
+ const hasResumableSessionId = (sid) => typeof sid === 'string' && sid.length > 0;
468
+ const refuseRequireExistingWithoutSession = (requireExisting, sessionId) => {
469
+ if (requireExisting !== true || hasResumableSessionId(sessionId))
470
+ return;
471
+ const shape = sessionId === undefined ? '缺席' : typeof sessionId === 'string' ? '空串' : coarseShape(sessionId);
472
+ throw new TypeError(`buildTaskRequest: requireExistingSession: true 要求续一个已存在的会话,但 sessionId ${shape} —— 没有可续的会话。` +
473
+ '上游对这一形同样拒收(不会新开会话,也不会续上任何东西);本层在装配前拒,不让它走一次往返。' +
474
+ '处置:要续哪个会话,就把它的 id 交在 sessionId 上;要开新会话,就别带 requireExistingSession(或给 false)。错误文本只报形状,不回显值。');
475
+ };
457
476
  const refuseBadMaxTokens = (v) => {
458
477
  if (v === undefined || v === null)
459
478
  return;
@@ -523,7 +542,7 @@ export function parseMaxCostUsd(text) {
523
542
  return { ok: false, why: 'not_positive' };
524
543
  return { ok: true, value };
525
544
  }
526
- const ACCESSOR_READ_ORDER = ['live', 'memoryCapture', 'memory', 'settings', 'maxCostUsd', 'excludeAllTools', 'maxTokens', 'outputRetries', 'fileHistory', 'inheritEnv'];
545
+ const ACCESSOR_READ_ORDER = ['live', 'memoryCapture', 'memory', 'settings', 'maxCostUsd', 'excludeAllTools', 'maxTokens', 'outputRetries', 'fileHistory', 'inheritEnv', 'requireExistingSession'];
527
546
  function dataFirstSnapshot(input, afterData) {
528
547
  let descs;
529
548
  try {
@@ -592,6 +611,7 @@ export function assembleTaskRequest(input, lane) {
592
611
  const outputRetriesIn = src.outputRetries;
593
612
  const fileHistoryIn = src.fileHistory;
594
613
  const inheritEnvIn = src.inheritEnv;
614
+ const requireExistingSessionIn = src.requireExistingSession;
595
615
  const INPUT_KEYS = Object.keys(INPUT_KEY_ROUTES);
596
616
  const i = snapshotByName(src, INPUT_KEYS, {
597
617
  live,
@@ -603,6 +623,7 @@ export function assembleTaskRequest(input, lane) {
603
623
  outputRetries: outputRetriesIn,
604
624
  fileHistory: fileHistoryIn,
605
625
  inheritEnv: inheritEnvIn,
626
+ requireExistingSession: requireExistingSessionIn,
606
627
  settings: settingsIn,
607
628
  });
608
629
  const SETTINGS_KEYS = Object.keys(SETTINGS_KEY_ROUTES);
@@ -615,6 +636,7 @@ export function assembleTaskRequest(input, lane) {
615
636
  refuseBadOutputRetries(outputRetriesIn);
616
637
  refuseBadFileHistory(fileHistoryIn);
617
638
  refuseBadInheritEnv(inheritEnvIn);
639
+ refuseBadRequireExistingSession(requireExistingSessionIn);
618
640
  const rewindRoute = INPUT_KEY_ROUTES.rewind;
619
641
  const RETIRED_REWIND_FILES_KEY = 'rewindFiles';
620
642
  const REWIND_KEYS = rewindRoute.via === 'matrix' ? rewindRoute.field.split('/') : [];
@@ -698,6 +720,7 @@ export function assembleTaskRequest(input, lane) {
698
720
  '想问某个键有没有座 ⇒ taskRequestSeatOf(key, lane)。`settings` 快照里的开放集子键放进 settings.resolved。' +
699
721
  '错误文本只报键名,不回显值。');
700
722
  }
723
+ refuseRequireExistingWithoutSession(requireExistingSessionIn, i.sessionId);
701
724
  refuseUtilityWithoutToolUnload(lane, excludeAllToolsIn);
702
725
  const carries = (route, value, key) => {
703
726
  if (route.via === 'always')
@@ -772,6 +795,7 @@ export function assembleTaskRequest(input, lane) {
772
795
  ...(on('memory') ? { memory: 'off' } : {}),
773
796
  ...(on('fileHistory') ? { fileHistory: FILE_HISTORY_DISABLED } : {}),
774
797
  ...(on('inheritEnv') ? { inheritEnv: inheritEnvIn } : {}),
798
+ ...(on('requireExistingSession') ? { requireExistingSession: true } : {}),
775
799
  ...(Object.keys(settingsOut).length > 0 ? { settings: settingsOut } : {}),
776
800
  };
777
801
  return Object.freeze({ request: out, omitted: Object.freeze([...omitted, ...lateOmitted]) });
@@ -19,13 +19,15 @@ export interface ResumePolicyBlockDetail {
19
19
  }
20
20
  export declare function resumePolicyBlockFromError(err: unknown): ResumePolicyBlockDetail | null;
21
21
  export declare function resumePolicyBlockContent(detail: ResumePolicyBlockDetail): string;
22
- export interface ResumeReopenDetail {
23
- code: string;
24
- reason: 'env_failed' | 'tool_unavailable' | 'tool_contract_mismatch';
25
- retriable: true;
22
+ export { resumeReopenFromError, resumeReopenContent } from './hitl/suspendedReopen.js';
23
+ export type { ResumeReopenDetail } from './hitl/suspendedReopen.js';
24
+ export type RequireExistingSessionRefusalForm = 'session_not_found' | 'session_id_missing';
25
+ export interface RequireExistingSessionRefusalDetail {
26
+ readonly code: string;
27
+ readonly form: RequireExistingSessionRefusalForm;
26
28
  }
27
- export declare function resumeReopenFromError(err: unknown): ResumeReopenDetail | null;
28
- export declare function resumeReopenContent(detail: ResumeReopenDetail): string;
29
+ export declare function requireExistingSessionRefusalFromError(err: unknown): RequireExistingSessionRefusalDetail | null;
30
+ export declare function requireExistingSessionRefusalContent(detail: Pick<RequireExistingSessionRefusalDetail, 'form'>): string;
29
31
  export interface ResumeContextUnavailableDetail {
30
32
  code: typeof RESUME_CONTEXT_UNAVAILABLE;
31
33
  staleAfterSec?: number;
@@ -1,5 +1,5 @@
1
1
  import { readErrorNumber, readErrorString } from './wireFailureShape.js';
2
- import { RESUME_CONTEXT_UNAVAILABLE, RESUME_PLACEMENT_MISMATCH, RESUME_PREFLIGHT_REJECTED, RESUME_REOPEN_CODES, RESUME_ENV_FAILED, RESUME_TOOL_UNAVAILABLE, RESUME_POLICY_FOLD_CODE, PARKED_RESUME_STARTUP_FAILED, isStatusDerivedCoarseCode, } from './engineErrorCodes.js';
2
+ import { RESUME_CONTEXT_UNAVAILABLE, RESUME_PLACEMENT_MISMATCH, RESUME_PREFLIGHT_REJECTED, RESUME_POLICY_FOLD_CODE, PARKED_RESUME_STARTUP_FAILED, isStatusDerivedCoarseCode, RESUME_SESSION_NOT_FOUND, } from './engineErrorCodes.js';
3
3
  import { capForDisplay } from './fleetTaskDesc.js';
4
4
  import { resumeRetryLaterFromError } from './wireErrorTriage.js';
5
5
  export const RESUME_REFUSAL_CODES = Object.freeze([
@@ -85,28 +85,50 @@ export function resumePolicyBlockContent(detail) {
85
85
  "This refusal does not say whether anything was consumed before it or whether a retry would pass — check the task's current state before deciding again, and ask whoever operates this deployment if it keeps happening",
86
86
  ].join(' · ');
87
87
  }
88
- export function resumeReopenFromError(err) {
88
+ export { resumeReopenFromError, resumeReopenContent } from './hitl/suspendedReopen.js';
89
+ const RESUME_SESSION_NOT_FOUND_TAIL = `(${RESUME_SESSION_NOT_FOUND})`;
90
+ const REQUIRE_EXISTING_BY_STATUS = new Map([
91
+ [404, Object.freeze({ form: 'session_not_found', coarse: 'not_found.resource' })],
92
+ [422, Object.freeze({ form: 'session_id_missing', coarse: 'request.unprocessable' })],
93
+ ]);
94
+ export function requireExistingSessionRefusalFromError(err) {
89
95
  if (typeof err !== 'object' || err === null)
90
96
  return null;
91
- const code = err.errorCode;
92
- if (typeof code !== 'string' || !RESUME_REOPEN_CODES.includes(code))
93
- return null;
94
- if (err.retriable !== true)
97
+ try {
98
+ const o = err;
99
+ const status = o['status'];
100
+ const byStatus = typeof status === 'number' ? REQUIRE_EXISTING_BY_STATUS.get(status) : undefined;
101
+ if (byStatus === undefined)
102
+ return null;
103
+ const code = o['errorCode'];
104
+ if (code === RESUME_SESSION_NOT_FOUND)
105
+ return Object.freeze({ code: RESUME_SESSION_NOT_FOUND, form: byStatus.form });
106
+ if (code !== byStatus.coarse)
107
+ return null;
108
+ const message = o['message'];
109
+ return typeof message === 'string' && message.endsWith(RESUME_SESSION_NOT_FOUND_TAIL)
110
+ ? Object.freeze({ code: RESUME_SESSION_NOT_FOUND, form: byStatus.form })
111
+ : null;
112
+ }
113
+ catch {
95
114
  return null;
96
- const reason = code.slice('resume.'.length);
97
- return { code, reason, retriable: true };
115
+ }
98
116
  }
99
- export function resumeReopenContent(detail) {
100
- const first = detail.code === RESUME_ENV_FAILED
101
- ? 'The execution environment for this parked work could not be brought back'
102
- : detail.code === RESUME_TOOL_UNAVAILABLE
103
- ? 'The tool this approval was granted for is not available on this engine'
104
- : 'The tool this approval was granted for has changed since the approval was recorded';
105
- return [
106
- first,
107
- 'The engine re-parked the work before consuming anything — the approval is still pending and your decision was not consumed',
108
- 'Re-fetch the pending approvals and decide again on an engine where the same tool is available',
109
- ].join(' · ');
117
+ const REQUIRE_EXISTING_SESSION_SENTENCES = Object.freeze({
118
+ session_not_found: 'The engine did not continue this conversation: the conversation it was asked to continue does not exist there (it may have expired, been removed, or never been created on that engine), and this request asked to continue only an existing one, so no new conversation was started · Start a new conversation, or pick another one to continue',
119
+ session_id_missing: 'The engine did not continue this conversation: this request asked to continue only an existing conversation but named none, so there was nothing to continue and no new conversation was started · Name the conversation to continue, or start a new one',
120
+ });
121
+ const REQUIRE_EXISTING_SESSION_GENERIC = 'The engine did not continue this conversation: this request asked to continue only an existing conversation and there was none it could continue, so no new conversation was started · Start a new conversation, or pick another one to continue';
122
+ export function requireExistingSessionRefusalContent(detail) {
123
+ try {
124
+ const form = detail?.form;
125
+ return typeof form === 'string' && Object.hasOwn(REQUIRE_EXISTING_SESSION_SENTENCES, form)
126
+ ? REQUIRE_EXISTING_SESSION_SENTENCES[form]
127
+ : REQUIRE_EXISTING_SESSION_GENERIC;
128
+ }
129
+ catch {
130
+ return REQUIRE_EXISTING_SESSION_GENERIC;
131
+ }
110
132
  }
111
133
  export function resumeContextUnavailableFromError(err) {
112
134
  if (typeof err !== 'object' || err === null)
@@ -1,5 +1,6 @@
1
1
  import { engineWireTarget } from './engineWireTarget.js';
2
2
  import { createEngineCapReader } from './engineCapReader.js';
3
+ import { _wireConversationRewindJudge } from './toolHistoryMismatch.js';
3
4
  const CAPS_KEYS = {
4
5
  conversationAnchor: 'resumeAt',
5
6
  forkRestore: 'rewindFiles',
@@ -37,8 +38,12 @@ const reader = createEngineCapReader({
37
38
  makeUnobserved: () => ({ kind: 'unobserved' }),
38
39
  });
39
40
  export function noteEngineCapsForRewindArchive(baseUrl, caps, opts) {
41
+ _wireConversationRewindJudge(conversationRewindJudge);
40
42
  reader.note(baseUrl, caps, opts);
41
43
  }
44
+ function conversationRewindJudge(engineBaseUrl) {
45
+ return rewindConversationAvailability(observedRewindArchive(engineBaseUrl));
46
+ }
42
47
  export function observedRewindArchive(baseUrl = engineWireTarget()?.baseUrl) {
43
48
  return reader.observed(baseUrl);
44
49
  }
package/dist/seam.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage
3
3
  import type { RunCostReconcile } from './adapter/downstream/terminalToSdkResult.js';
4
4
  import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView, ContextUsageSectionView, PromptManifestSectionView, PromptManifestBlockView, PromptManifestToolView } from './adapter/downstream/eventToSdkMessage.js';
5
5
  import type { ToolRosterView, ToolRosterDeltaView, WiringManifestHandsView } from './toolRoster.js';
6
+ import type { SuspendedReopenReading } from './hitl/suspendedReopen.js';
6
7
  export interface AbortSignalLike {
7
8
  readonly aborted: boolean;
8
9
  }
@@ -149,7 +150,16 @@ export type ChromeEvent = {
149
150
  } | ({
150
151
  kind: 'run_cost_reconciled';
151
152
  laneProof: LaneProof;
152
- } & RunCostReconcile) | HumanInputChromeEvent | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | CompactionOutcomeChromeEvent | PromptAssembledChromeEvent | MemorySavedChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent | ApprovalRequestChromeEvent | ApprovalRevokeChromeEvent;
153
+ } & RunCostReconcile) | HumanInputChromeEvent | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | CompactionOutcomeChromeEvent | PromptAssembledChromeEvent | MemorySavedChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent | ApprovalRequestChromeEvent | ApprovalRevokeChromeEvent | {
154
+ kind: 'suspended_reopened';
155
+ laneProof: LaneProof;
156
+ reopen: Extract<SuspendedReopenReading, {
157
+ kind: 'reopened';
158
+ }>;
159
+ content?: string;
160
+ eventId?: string;
161
+ taskId?: string;
162
+ };
153
163
  export interface WiringManifestChromeEvent {
154
164
  kind: 'wiring_manifest';
155
165
  laneProof: LaneProof;
@@ -163,6 +173,7 @@ export interface WiringManifestChromeEvent {
163
173
  autoConsolidation?: WiringManifestAutoConsolidationView;
164
174
  readDeny?: WiringManifestReadDenyView;
165
175
  hands?: WiringManifestHandsView;
176
+ fileHistory?: string;
166
177
  eventId?: string;
167
178
  }
168
179
  export interface TextSegmentCommittedRow {
package/dist/seam.js CHANGED
@@ -126,6 +126,13 @@ const CHROME_ARM_TABLE = {
126
126
  '🔴 账本重放的历史帧只用于时间线渲染,卡集的全量对账基准是开流 preamble(它带的 expiresInMs 是铸帧时刻的旧值);' +
127
127
  'card 的 message/args 是模型/工具产文,只渲染永不回喂模型',
128
128
  },
129
+ suspended_reopened: {
130
+ required: false,
131
+ duty: '可选:引擎确认把一次已送出的决定重新挂回待决 ⇒ 上屏一句「决定没被消费、再决一次」(`content`,单源句;缺席 = 码不在 reopen 族闭集,可照 `reopen.code` 原词显示,开集按成员渲染必带 default 臂)。' +
132
+ '🔴 按 `eventId` 幂等(同一条流的重放本包已去重;前台腿与接管腿各发一条、同 eventId,去重归宿主);' +
133
+ '🔴 接管 / 从头重放的腿:本臂之后同流又来「门已不在」的帧(text / tool_start / turn_end / done / failed 或新的挂起)⇒ 那次重开是历史,别当现状上屏;会话已切走的别补发 —— 这两件归宿主判;' +
134
+ '明说没重开 / 老引擎缺键一条都不发,本臂没来 ≠ 确认没重开;`taskId` 只在帧上自带时在场',
135
+ },
129
136
  approval_revoke: {
130
137
  required: true,
131
138
  duty: '用 sdk isApprovalRevokeFrameV1 窄化 frame 取 batchId/askIds/taskId;窄不下来按通用撤卡处理(该批本地卡照清);' +
@@ -0,0 +1,76 @@
1
+ import type { EngineWireClientConfig } from './engineWireSdk.js';
2
+ import type { MemoryEraseReceiptReading, MemoryEraseUnreadableCause } from './memoryEntriesWire.js';
3
+ export type SessionMemoryEraseWire = Pick<EngineWireClientConfig, 'baseUrl' | 'token' | 'principal' | 'timeoutMs' | 'fetchImpl'>;
4
+ export interface SessionMemoryEraseCall {
5
+ sessionId: string;
6
+ requestId: string;
7
+ allowUnevidenced?: boolean;
8
+ }
9
+ export interface SessionMemoryEraseOptions {
10
+ signal?: AbortSignal | null;
11
+ }
12
+ export interface SessionMemoryEraseSent {
13
+ sessionId: string;
14
+ requestId: string;
15
+ allowUnevidenced?: boolean;
16
+ }
17
+ export type SessionMemoryEraseNotSentWhy = 'bad_input' | 'no_connection' | 'cancelled';
18
+ export type SessionMemoryEraseJson = null | boolean | number | string | readonly SessionMemoryEraseJson[] | {
19
+ readonly [key: string]: SessionMemoryEraseJson;
20
+ };
21
+ export type SessionMemoryEraseAnswer = {
22
+ kind: 'not_sent';
23
+ why: SessionMemoryEraseNotSentWhy;
24
+ detail: string;
25
+ } | {
26
+ kind: 'answered';
27
+ sent: SessionMemoryEraseSent;
28
+ status: number;
29
+ body: SessionMemoryEraseJson | undefined;
30
+ } | {
31
+ kind: 'no_answer';
32
+ sent: SessionMemoryEraseSent;
33
+ detail: string;
34
+ };
35
+ export declare function eraseSessionMemory(call: SessionMemoryEraseCall, wire: SessionMemoryEraseWire, opts?: SessionMemoryEraseOptions): Promise<SessionMemoryEraseAnswer>;
36
+ export type SessionMemoryEraseRefusalReason = 'bad_input' | 'no_connection' | 'cancelled_before_send' | 'credentials_required' | 'not_permitted' | 'memory_engine_absent' | 'session_not_found' | 'route_absent' | 'request_rejected' | 'evidence_unavailable' | 'request_id_reused' | 'store_unreadable';
37
+ export type SessionMemoryEraseUnconfirmedWhy = 'no_answer' | 'receipt_unreadable' | 'receipt_mismatch' | 'store_link_outside' | 'control_plane_corrupt' | 'engine_fault' | 'unclassified' | 'no_code' | 'unreadable';
38
+ export type SessionMemoryEraseRetry = 'not_needed' | 'same_request_id' | 'new_request_id' | 'needs_operator' | 'needs_consent' | 'human_decision' | 'pointless';
39
+ type SessionMemoryEraseReceipt = Extract<MemoryEraseReceiptReading, {
40
+ kind: 'present';
41
+ }>;
42
+ export type SessionMemoryEraseOutcome = {
43
+ kind: 'erased';
44
+ receipt: SessionMemoryEraseReceipt;
45
+ text: string;
46
+ retry: 'not_needed';
47
+ retryText: string;
48
+ } | {
49
+ kind: 'partial';
50
+ receipt: SessionMemoryEraseReceipt;
51
+ text: string;
52
+ retry: 'same_request_id' | 'human_decision';
53
+ retryText: string;
54
+ } | {
55
+ kind: 'refused';
56
+ reason: SessionMemoryEraseRefusalReason;
57
+ text: string;
58
+ retry: SessionMemoryEraseRetry;
59
+ retryText: string;
60
+ status?: number;
61
+ errorCode?: string;
62
+ } | {
63
+ kind: 'unconfirmed';
64
+ why: SessionMemoryEraseUnconfirmedWhy;
65
+ text: string;
66
+ retry: SessionMemoryEraseRetry;
67
+ retryText: string;
68
+ status?: number;
69
+ errorCode?: string;
70
+ receiptWhy?: MemoryEraseUnreadableCause;
71
+ };
72
+ export declare const SESSION_MEMORY_ERASE_REFUSAL_REASONS: readonly SessionMemoryEraseRefusalReason[];
73
+ export declare const SESSION_MEMORY_ERASE_UNCONFIRMED_WHYS: readonly SessionMemoryEraseUnconfirmedWhy[];
74
+ export declare const SESSION_MEMORY_ERASE_RETRIES: readonly SessionMemoryEraseRetry[];
75
+ export declare function sessionMemoryEraseOutcome(answer: SessionMemoryEraseAnswer): SessionMemoryEraseOutcome;
76
+ export {};