@sema-agent/client-core 0.89.0 → 0.91.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 (104) hide show
  1. package/CHANGELOG.md +297 -0
  2. package/README.md +35 -24
  3. package/dist/adapt/arms.js +9 -0
  4. package/dist/adapter/downstream/eventToSdkMessage.js +4 -2
  5. package/dist/adapter/downstream/wiringManifestView.d.ts +6 -0
  6. package/dist/adapter/downstream/wiringManifestView.js +16 -0
  7. package/dist/adapter/runStream.d.ts +2 -0
  8. package/dist/adapter/runStream.js +180 -38
  9. package/dist/askSurvivesStandingAnswer.d.ts +10 -0
  10. package/dist/askSurvivesStandingAnswer.js +40 -0
  11. package/dist/attachmentSeatCapability.d.ts +11 -0
  12. package/dist/attachmentSeatCapability.js +27 -0
  13. package/dist/attachmentsWireCaps.d.ts +22 -2
  14. package/dist/attachmentsWireCaps.js +95 -22
  15. package/dist/compensations.js +10 -14
  16. package/dist/controlRouter.d.ts +9 -2
  17. package/dist/controlRouter.js +66 -28
  18. package/dist/coreValuePorts.d.ts +1 -0
  19. package/dist/decideFailureNote.js +4 -2
  20. package/dist/disableSkillsWire.d.ts +1 -0
  21. package/dist/disableSkillsWire.js +8 -1
  22. package/dist/displayUntrusted.js +153 -4
  23. package/dist/engineCapReader.js +12 -1
  24. package/dist/engineCapsCache.d.ts +3 -1
  25. package/dist/engineCapsCache.js +56 -6
  26. package/dist/engineClosedSets.d.ts +2 -0
  27. package/dist/engineClosedSets.js +3 -0
  28. package/dist/engineErrorCodes.d.ts +3 -0
  29. package/dist/engineErrorCodes.js +10 -0
  30. package/dist/engineNoticeCodes.d.ts +2 -9
  31. package/dist/engineNoticeCodes.js +19 -27
  32. package/dist/fileHistoryLegNotice.d.ts +20 -0
  33. package/dist/fileHistoryLegNotice.js +122 -0
  34. package/dist/gateOutcome.d.ts +1 -1
  35. package/dist/gateOutcome.js +1 -0
  36. package/dist/generated/engineFactTables.d.ts +13 -0
  37. package/dist/generated/engineFactTables.js +50 -0
  38. package/dist/generated/engineNoticeTables.js +0 -3
  39. package/dist/hitl/askGateWire.d.ts +1 -1
  40. package/dist/hitl/askGateWire.js +1 -1
  41. package/dist/hitl/crashConverged.d.ts +7 -0
  42. package/dist/hitl/crashConverged.js +57 -0
  43. package/dist/hitl/crashConvergedNotice.d.ts +4 -0
  44. package/dist/hitl/crashConvergedNotice.js +57 -0
  45. package/dist/hitl/hitlBridge.d.ts +7 -4
  46. package/dist/hitl/parkResolver.d.ts +7 -2
  47. package/dist/hitl/parkResolver.js +19 -7
  48. package/dist/hitl/planReviewWire.d.ts +1 -1
  49. package/dist/hitl/planReviewWire.js +18 -4
  50. package/dist/hitl/sessionPolicyWire.d.ts +5 -0
  51. package/dist/hitl/sessionPolicyWire.js +63 -0
  52. package/dist/hitl/suspendedReopen.js +2 -2
  53. package/dist/hitl/toolApprovalWire.d.ts +4 -1
  54. package/dist/hitl/toolApprovalWire.js +20 -0
  55. package/dist/hookMatcherVerdict.d.ts +29 -0
  56. package/dist/hookMatcherVerdict.js +265 -0
  57. package/dist/hookToolAliases.d.ts +3 -0
  58. package/dist/hookToolAliases.js +10 -0
  59. package/dist/humanTurn.d.ts +91 -0
  60. package/dist/humanTurn.js +253 -0
  61. package/dist/humanTurnRead.d.ts +105 -0
  62. package/dist/humanTurnRead.js +331 -0
  63. package/dist/index.d.ts +30 -5
  64. package/dist/index.js +19 -4
  65. package/dist/inheritEnvWire.js +1 -1
  66. package/dist/liveModelCatalog.d.ts +2 -0
  67. package/dist/liveModelCatalog.js +51 -3
  68. package/dist/permissionRuleIssue.d.ts +1 -1
  69. package/dist/permissionRuleIssue.js +22 -3
  70. package/dist/permissionsAllowGrantCapability.d.ts +18 -0
  71. package/dist/permissionsAllowGrantCapability.js +84 -0
  72. package/dist/permissionsDeclaration.d.ts +48 -0
  73. package/dist/permissionsDeclaration.js +307 -0
  74. package/dist/promptDelivery.d.ts +16 -5
  75. package/dist/promptDelivery.js +172 -32
  76. package/dist/providerRejection.d.ts +3 -1
  77. package/dist/providerRejection.js +27 -1
  78. package/dist/readDenyTiers.d.ts +10 -0
  79. package/dist/readDenyTiers.js +29 -0
  80. package/dist/request/taskRequest.js +27 -1
  81. package/dist/resumeRefusalCopy.d.ts +11 -0
  82. package/dist/resumeRefusalCopy.js +61 -4
  83. package/dist/rewindArchiveCapability.js +4 -4
  84. package/dist/rewindWireCaps.d.ts +2 -1
  85. package/dist/rewindWireCaps.js +19 -2
  86. package/dist/sdkRegistryTransit.d.ts +1 -0
  87. package/dist/sdkRegistryTransit.js +1 -0
  88. package/dist/seam.d.ts +2 -0
  89. package/dist/supportFloor.d.ts +10 -0
  90. package/dist/supportFloor.js +42 -0
  91. package/dist/taskRequestWords.d.ts +7 -0
  92. package/dist/taskRequestWords.js +24 -0
  93. package/dist/toolHistoryBadTurn.d.ts +1 -0
  94. package/dist/toolHistoryBadTurn.js +89 -0
  95. package/dist/toolHistoryMismatch.d.ts +2 -1
  96. package/dist/toolHistoryMismatch.js +19 -6
  97. package/dist/userAttachmentEnvelope.d.ts +61 -0
  98. package/dist/userAttachmentEnvelope.js +204 -0
  99. package/dist/wireFailureShape.d.ts +1 -0
  100. package/dist/wireFailureShape.js +4 -0
  101. package/dist/wireRefusalCopy.d.ts +6 -0
  102. package/dist/wireRefusalCopy.js +26 -3
  103. package/docs/INTEGRATION-CLIENTS.md +1369 -18
  104. package/package.json +3 -3
@@ -1,11 +1,11 @@
1
1
  import { hostEnv } from './hostEnv.js';
2
2
  import { envFlagOff } from './envFlag.js';
3
+ import { retiredAttachmentsKeyOf } from './taskRequestWords.js';
3
4
  export const ATTACHMENTS_ENV = 'SEMA_ATTACHMENTS';
4
5
  export const ATTACHMENTS_SPEC_KEYS = Object.freeze([
5
6
  'todoReminder',
6
7
  'todoReminderMode',
7
8
  'changedFiles',
8
- 'planModeReminder',
9
9
  'budgetUsd',
10
10
  'backgroundTasks',
11
11
  'toolsDelta',
@@ -17,7 +17,6 @@ export const ATTACHMENTS_DEFAULT_ON_KEYS = ['agentListing', 'skillsListing', 'ba
17
17
  const OPT_IN_KEYS = [
18
18
  'todoReminder',
19
19
  'changedFiles',
20
- 'planModeReminder',
21
20
  'budgetUsd',
22
21
  'backgroundTasks',
23
22
  'toolsDelta',
@@ -28,25 +27,48 @@ const DEFAULT_PAIR = ['backgroundTasks', 'toolsDelta'];
28
27
  const _attachmentsKeyPins = [true, true, true, true, true, true];
29
28
  void _attachmentsKeyPins;
30
29
  const TODO_REMINDER_MODES = ['baseline', 'off'];
30
+ export class AttachmentsSpecRefused extends TypeError {
31
+ token;
32
+ why;
33
+ allowed;
34
+ constructor(message, init) {
35
+ super(message);
36
+ this.name = 'AttachmentsSpecRefused';
37
+ this.token = init.token;
38
+ this.why = init.why;
39
+ this.allowed = Object.freeze([...init.allowed]);
40
+ if (init.key !== undefined)
41
+ this.key = init.key;
42
+ }
43
+ }
44
+ function legalAttachmentsTokens() {
45
+ return ['off', 'full', 'all', 'default', '<literalTrueKey>', '+<literalTrueKey>', ...DEFAULT_ON_ONLY_KEYS.map(k => `-${k}`), `todoReminderMode=${TODO_REMINDER_MODES.join('|')}`];
46
+ }
31
47
  function refuse(token, why) {
32
- throw new Error(`${ATTACHMENTS_ENV}: refusing token "${token}" — ${why}. ` +
33
- `Legal tokens: off | full | all | default | <literalTrueKey> | +<literalTrueKey> | ` +
34
- `-agentListing | -skillsListing | todoReminderMode=baseline|off`);
48
+ const allowed = legalAttachmentsTokens();
49
+ throw new AttachmentsSpecRefused(`${ATTACHMENTS_ENV}: refusing token "${token}" — ${why}. Legal tokens: ${allowed.join(' | ')}`, { token, why, allowed });
35
50
  }
36
- export function attachmentsForRequest(env = hostEnv()) {
37
- const raw = (env[ATTACHMENTS_ENV] ?? '').trim().toLowerCase();
51
+ function deny(kind, token, why, index, key) {
52
+ return { ok: false, refusal: { kind, token, why, entry: index + 1, key } };
53
+ }
54
+ function judgeAttachmentsEnv(raw) {
38
55
  if (envFlagOff(raw))
39
- return allDefaultOnOff();
56
+ return { ok: true, spec: allDefaultOnOff() };
40
57
  const tokens = raw.split(',').map(t => t.trim()).filter(t => t.length > 0);
58
+ for (const [i, token] of tokens.entries()) {
59
+ const named = retiredAttachmentsKeyOf(token);
60
+ if (named !== undefined)
61
+ return { ok: false, refusal: { kind: 'retired_key', token, why: named.why, entry: i + 1, key: named.key, retired: named } };
62
+ }
41
63
  if (tokens.length === 0)
42
- return { backgroundTasks: true, toolsDelta: true };
64
+ return { ok: true, spec: { backgroundTasks: true, toolsDelta: true } };
43
65
  if (tokens.length === 1 && (tokens[0] === 'full' || tokens[0] === 'all'))
44
- return allLiteralTrue();
66
+ return { ok: true, spec: allLiteralTrue() };
45
67
  if (tokens.length === 1 && !isKnownToken(tokens[0])) {
46
- return { backgroundTasks: true, toolsDelta: true };
68
+ return { ok: true, spec: { backgroundTasks: true, toolsDelta: true } };
47
69
  }
48
70
  const spec = {};
49
- for (const token of tokens) {
71
+ for (const [i, token] of tokens.entries()) {
50
72
  if (token === 'full' || token === 'all') {
51
73
  Object.assign(spec, allLiteralTrue());
52
74
  continue;
@@ -59,11 +81,11 @@ export function attachmentsForRequest(env = hostEnv()) {
59
81
  if (token.startsWith('-')) {
60
82
  const key = canonicalKey(token.slice(1));
61
83
  if (key === undefined)
62
- refuse(token, 'unknown attachments key');
84
+ return deny('unknown_key', token, 'unknown attachments key', i, undefined);
63
85
  if (!isDefaultOnKey(key)) {
64
- refuse(token, `"${key}" belongs to the literal-true family whose OFF is "delete the key", not "send false" ` +
86
+ return deny('minus_on_literal_true_key', token, `"${key}" belongs to the literal-true family whose OFF is "delete the key", not "send false" ` +
65
87
  '(core wire contract, board [479] ask-2) — simply do not turn it on. ' +
66
- `Only the core DEFAULT-ON keys (${ATTACHMENTS_DEFAULT_ON_KEYS.join(' / ')}) accept an explicit false`);
88
+ `Only the core DEFAULT-ON keys (${ATTACHMENTS_DEFAULT_ON_KEYS.join(' / ')}) accept an explicit false`, i, key);
67
89
  }
68
90
  spec[key] = false;
69
91
  continue;
@@ -71,25 +93,76 @@ export function attachmentsForRequest(env = hostEnv()) {
71
93
  const [lhs, rhs] = splitAssign(token);
72
94
  if (rhs !== undefined) {
73
95
  if (canonicalKey(lhs) !== 'todoReminderMode')
74
- refuse(token, 'only `todoReminderMode` takes a `key=value` form');
96
+ return deny('value_form_on_other_key', token, 'only `todoReminderMode` takes a `key=value` form', i, canonicalKey(lhs));
75
97
  const mode = TODO_REMINDER_MODES.find(v => v === rhs);
76
98
  if (mode === undefined)
77
- refuse(token, `todoReminderMode must be one of ${TODO_REMINDER_MODES.join('|')}`);
99
+ return deny('todo_mode_bad_value', token, `todoReminderMode must be one of ${TODO_REMINDER_MODES.join('|')}`, i, 'todoReminderMode');
78
100
  spec.todoReminderMode = mode;
79
101
  continue;
80
102
  }
81
103
  const key = canonicalKey(lhs.startsWith('+') ? lhs.slice(1) : lhs);
82
104
  if (key === undefined)
83
- refuse(token, 'unknown attachments key');
105
+ return deny('unknown_key', token, 'unknown attachments key', i, undefined);
84
106
  if (isDefaultOnOnlyKey(key)) {
85
- refuse(token, `"${key}" is core DEFAULT-ON — turning it "on" is a no-op that would send a redundant true; ` +
86
- `use "-${key}" to turn it OFF`);
107
+ return deny('plus_on_default_on_key', token, `"${key}" is core DEFAULT-ON — turning it "on" is a no-op that would send a redundant true; ` + `use "-${key}" to turn it OFF`, i, key);
87
108
  }
88
109
  if (key === 'todoReminderMode')
89
- refuse(token, 'todoReminderMode needs a value: `todoReminderMode=baseline|off`');
110
+ return deny('todo_mode_needs_value', token, 'todoReminderMode needs a value: `todoReminderMode=baseline|off`', i, key);
90
111
  spec[key] = true;
91
112
  }
92
- return spec;
113
+ return { ok: true, spec };
114
+ }
115
+ function envRawOf(env) {
116
+ return (env[ATTACHMENTS_ENV] ?? '').trim().toLowerCase();
117
+ }
118
+ export function attachmentsForRequest(env = hostEnv()) {
119
+ const judged = judgeAttachmentsEnv(envRawOf(env));
120
+ if (!judged.ok)
121
+ return refuse(judged.refusal.token, judged.refusal.why);
122
+ return judged.spec;
123
+ }
124
+ function attachmentsWayOut() {
125
+ const on = OPT_IN_KEYS.join(', ');
126
+ const off = ATTACHMENTS_DEFAULT_ON_KEYS.join(', ');
127
+ return (`Accepted writing is either the single word off on its own (every attachment off; it cannot be combined with other entries), or a comma-separated list of: ` +
128
+ `full or all (every opt-in attachment on), default, the name of an attachment to turn on (${on}), a leading minus to turn off one of ${off}, and todoReminderMode=${TODO_REMINDER_MODES.join(' or todoReminderMode=')}. ` +
129
+ `To drop the setting altogether, unset ${ATTACHMENTS_ENV}; the defaults then apply.`);
130
+ }
131
+ function attachmentsRefusalSentence(r) {
132
+ const head = `The attachments setting (${ATTACHMENTS_ENV}) cannot be used as it is set: entry ${r.entry} of the list`;
133
+ const key = r.key ?? '';
134
+ switch (r.kind) {
135
+ case 'retired_key':
136
+ return `${head} names "${key}", a key that no longer exists — ${r.retired?.because ?? 'it was retired'}, and a request carrying that key is refused by the server. Remove that entry${r.retired === undefined ? '' : `; ${r.retired.remedy}`}. ${attachmentsWayOut()}`;
137
+ case 'unknown_key':
138
+ return `${head} is not an attachment this version knows. ${attachmentsWayOut()}`;
139
+ case 'minus_on_literal_true_key':
140
+ return key === 'todoReminderMode'
141
+ ? `${head} puts a minus on todoReminderMode, which is a setting with a value rather than something to turn off — write todoReminderMode=${TODO_REMINDER_MODES.join(' or todoReminderMode=')}, or leave it out of the list. A minus only works on ${ATTACHMENTS_DEFAULT_ON_KEYS.join(', ')}, which are on by default. ${attachmentsWayOut()}`
142
+ : `${head} puts a minus on ${key}, and a minus cannot switch that attachment off — it is on only when it is named, or when full, all or default switches it on. A minus only works on ${ATTACHMENTS_DEFAULT_ON_KEYS.join(', ')}, which are on by default. To leave ${key} out, do not name it, and if full, all or default is what switches it on, list the attachments you want one by one instead. ${attachmentsWayOut()}`;
143
+ case 'plus_on_default_on_key':
144
+ return `${head} turns on ${key}, which is already on by default. To turn it off write -${key}; otherwise leave it out of the list. ${attachmentsWayOut()}`;
145
+ case 'value_form_on_other_key':
146
+ return `${head} uses a name=value form, and only todoReminderMode takes one. ${attachmentsWayOut()}`;
147
+ case 'todo_mode_bad_value':
148
+ return `${head} gives todoReminderMode a value it does not accept; the values are ${TODO_REMINDER_MODES.join(' and ')}. ${attachmentsWayOut()}`;
149
+ case 'todo_mode_needs_value':
150
+ return `${head} names todoReminderMode without a value; write todoReminderMode=${TODO_REMINDER_MODES.join(' or todoReminderMode=')}. ${attachmentsWayOut()}`;
151
+ }
152
+ }
153
+ function attachmentsUnreadableSentence() {
154
+ return `The attachments setting (${ATTACHMENTS_ENV}) could not be read as text, so it cannot be used as it is set. Set it to a plain comma-separated list, or unset it. ${attachmentsWayOut()}`;
155
+ }
156
+ export function attachmentsSubmitVerdict(env = hostEnv()) {
157
+ try {
158
+ const judged = judgeAttachmentsEnv(envRawOf(env));
159
+ if (judged.ok)
160
+ return { ok: true, attachments: judged.spec };
161
+ return { ok: false, sentence: attachmentsRefusalSentence(judged.refusal), refusedKey: judged.refusal.key };
162
+ }
163
+ catch {
164
+ return { ok: false, sentence: attachmentsUnreadableSentence(), refusedKey: undefined };
165
+ }
93
166
  }
94
167
  function allDefaultOnOff() {
95
168
  const out = {};
@@ -7,8 +7,7 @@ export const MIGRATED_COMPENSATIONS = [
7
7
  libraryHalf: 'adapt.ts bindTaskCard()',
8
8
  chromeArm: null,
9
9
  hostHalf: null,
10
- retireOn: 'P1-2 到货且达标线 ≥ server 1.290(typed parentToolCallId 在每条 tick 上恒在)后,' +
11
- 'FIFO 回落臂可删 —— 但只在**确认不再支持老引擎**时删,见 bindTaskCard 的 old-engine fallback 注释',
10
+ retireOn: 'RL-31',
12
11
  },
13
12
  {
14
13
  id: 'T34',
@@ -26,7 +25,7 @@ export const MIGRATED_COMPENSATIONS = [
26
25
  libraryHalf: 'adapt.ts fireSubagentStart()(类型恒记 + 一生一次去重 + if-started 门)',
27
26
  chromeArm: 'subagent_lifecycle',
28
27
  hostHalf: '真执行用户 hook 进程(浏览器宿主根本不能执行 ⇒ 这一半必须留端)',
29
- retireOn: 'G21 / P3a(引擎侧发正式的 subagent 生命周期事件)后,壳替 fire 整条退役',
28
+ retireOn: 'RL-32',
30
29
  },
31
30
  {
32
31
  id: 'T36',
@@ -35,7 +34,7 @@ export const MIGRATED_COMPENSATIONS = [
35
34
  libraryHalf: 'adapt.ts settlePanelTasks 的四个调用点(tool_end 关卡 / result / turn 末 / abort)',
36
35
  chromeArm: 'inline_task_stats',
37
36
  hostHalf: null,
38
- retireOn: 'W6(引擎为每条子代发终态 tick)后,防御 sweep 降为纯兜底',
37
+ retireOn: 'RL-33',
39
38
  },
40
39
  {
41
40
  id: 'T44',
@@ -53,7 +52,7 @@ export const MIGRATED_COMPENSATIONS = [
53
52
  libraryHalf: 'adapt.ts task_progress 臂的 workflowRunId / wa-id / workflow-card-parent 三级门',
54
53
  chromeArm: null,
55
54
  hostHalf: null,
56
- retireOn: 'P1-2 到货后判据②③(形状嗅探 + 实例台账)可删,只留判据①(帧带 workflowRunId)',
55
+ retireOn: 'RL-34',
57
56
  },
58
57
  {
59
58
  id: 'T48',
@@ -63,7 +62,7 @@ export const MIGRATED_COMPENSATIONS = [
63
62
  chromeArm: null,
64
63
  hostHalf: 'footer 行过滤 isFooterTaskRow/filterFooterTaskRows —— 含「丢弃所有无 parentId 顶层行」' +
65
64
  '这条**端专属决策**(主 run 头部行由端自己合成渲染),留 TUI',
66
- retireOn: 'P1-2 到货后 fleet 行自带 parentToolCallId ⇒ 双生行不需要按尾段查',
65
+ retireOn: 'RL-35',
67
66
  },
68
67
  {
69
68
  id: 'T1',
@@ -83,8 +82,7 @@ export const MIGRATED_COMPENSATIONS = [
83
82
  libraryHalf: 'notifications.ts taskNotificationDedupKey + taskNotificationDedupKeyFromWire(裸 wire 入口)',
84
83
  chromeArm: null,
85
84
  hostHalf: null,
86
- retireOn: 'P1-3 到货(帧带 completionId)⇒ 键改成**读** completionId,重算算法整条删;' +
87
- '删之前要先有一批「同一完成在两条通道上 completionId 相同」的实测(否则换成了一把更差的键)',
85
+ retireOn: 'RL-36',
88
86
  },
89
87
  {
90
88
  id: 'T3',
@@ -95,8 +93,7 @@ export const MIGRATED_COMPENSATIONS = [
95
93
  'adapt.ts importLedgerFromTranscript(消费口)',
96
94
  chromeArm: null,
97
95
  hostHalf: null,
98
- retireOn: 'P1-3 到货 ⇒ **回植仍要做**(crash 窗口客观存在),但不必再解 XML —— 转录里直接带 ' +
99
- 'completionId。所以退的是「XML 反解」这一段,不是整条回植',
96
+ retireOn: 'RL-37',
100
97
  },
101
98
  {
102
99
  id: 'T25',
@@ -106,7 +103,7 @@ export const MIGRATED_COMPENSATIONS = [
106
103
  libraryHalf: 'toolResult.ts 终态轮询卡臂的预 seed + notifications.markEngineWorkflowNotified',
107
104
  chromeArm: null,
108
105
  hostHalf: null,
109
- retireOn: 'P1-3 到货 ⇒ 两个方向带同一个 completionId,预 seed 这一手(按 runId 抢跑)可删',
106
+ retireOn: 'RL-38',
110
107
  },
111
108
  {
112
109
  id: 'T53',
@@ -115,8 +112,7 @@ export const MIGRATED_COMPENSATIONS = [
115
112
  libraryHalf: 'adapt.ts task_notification / workflow_complete 两臂的四道门(逐道说明见本行上方注释)',
116
113
  chromeArm: null,
117
114
  hostHalf: null,
118
- retireOn: 'P1-3 到货 ⇒ 降到「①渲染去重(键换 completionId)+ ④队列撤单」两道;②③ 删。' +
119
- '🔴 判据不是「四变一」而是逐道过一遍上面那四行 —— 把 ④ 当成去重删掉会让队列里的旧条目重新上屏',
115
+ retireOn: 'RL-39',
120
116
  },
121
117
  {
122
118
  id: 'T49',
@@ -125,7 +121,7 @@ export const MIGRATED_COMPENSATIONS = [
125
121
  libraryHalf: 'adapt.ts task_progress 臂 registerSubagentAlias(explicitParent, taskId)',
126
122
  chromeArm: null,
127
123
  hostHalf: null,
128
- retireOn: 'P1-2 到货后 content 帧自带 taskId ⇒ alias 学习整条可删',
124
+ retireOn: 'RL-40',
129
125
  },
130
126
  ];
131
127
  export function compensationSplitViolations() {
@@ -21,7 +21,12 @@ export type ControlSafetyCode = 'not_running' | 'invalid_content' | 'queue_full'
21
21
  export declare class ControlSafetyError extends Error {
22
22
  readonly code: ControlSafetyCode;
23
23
  readonly cause?: unknown | undefined;
24
- constructor(message: string, code: ControlSafetyCode, cause?: unknown | undefined);
24
+ readonly advice?: string;
25
+ readonly status?: number;
26
+ constructor(message: string, code: ControlSafetyCode, cause?: unknown | undefined, detail?: {
27
+ readonly advice?: string;
28
+ readonly status?: number;
29
+ });
25
30
  }
26
31
  export type QueuePriority = 'now' | 'next' | 'later';
27
32
  export interface QueuedCommand {
@@ -72,8 +77,10 @@ export interface ErrCodes {
72
77
  }
73
78
  export declare function errCodes(e: unknown): ErrCodes;
74
79
  export declare function steeringStatusIs4xx(e: object): boolean;
75
- export interface SteeringRefusedBeforeEngine {
80
+ export interface SteerRefusalAdvice {
76
81
  safety: ControlSafetyCode;
77
82
  advice: string;
78
83
  }
84
+ export type SteeringRefusedBeforeEngine = SteerRefusalAdvice;
79
85
  export declare function steeringRefusedBeforeEngine(e: object): SteeringRefusedBeforeEngine | undefined;
86
+ export declare function steerRefusalAdviceOf(e: object): SteerRefusalAdvice | undefined;
@@ -3,11 +3,17 @@ import { uuid } from './adapter/types.js';
3
3
  export class ControlSafetyError extends Error {
4
4
  code;
5
5
  cause;
6
- constructor(message, code, cause) {
6
+ constructor(message, code, cause, detail) {
7
7
  super(message);
8
8
  this.code = code;
9
9
  this.cause = cause;
10
10
  this.name = 'ControlSafetyError';
11
+ const advice = detail?.advice;
12
+ const status = detail?.status;
13
+ if (typeof advice === 'string' && advice.length > 0)
14
+ this.advice = advice;
15
+ if (typeof status === 'number')
16
+ this.status = status;
11
17
  }
12
18
  }
13
19
  const BAND_RANK = { now: 0, next: 1, later: 2 };
@@ -55,39 +61,38 @@ export class ControlRouter {
55
61
  }
56
62
  async steer(text, mode, opts) {
57
63
  if (this.taskId === null) {
58
- throw new ControlSafetyError('no run in flight to steer — submit a turn first', 'not_running');
64
+ throw controlSafetyRefusal('not_running', 'no run in flight to steer', 'submit a turn first');
59
65
  }
60
66
  const body = mode !== undefined ? { text, mode } : { text };
61
67
  try {
62
68
  return await this.client.runs.steer(this.taskId, body, opts?.signal !== undefined ? { signal: opts.signal } : undefined);
63
69
  }
64
70
  catch (e) {
65
- const verdict = steeringVerdict(e);
66
- if (verdict !== undefined) {
67
- const safety = steeringSafetyOf(e, verdict);
68
- const wireCode = errCodes(e).code ?? errCodes(e).name ?? 'steering.<unknown>';
69
- throw new ControlSafetyError(`${wireCode} — ${steeringAdviceOf(e, safety) ?? 'steering refused'}`, safety, e);
71
+ if (typeof e === 'object' && e !== null) {
72
+ const refusal = steerRefusalAdviceOf(e);
73
+ if (refusal !== undefined)
74
+ throw controlSafetyRefusal(refusal.safety, steerRefusalWireLabel(e), refusal.advice, e);
70
75
  }
71
76
  throw e;
72
77
  }
73
78
  }
74
79
  async cancel(opts) {
75
80
  if (this.taskId === null) {
76
- throw new ControlSafetyError('no run in flight to cancel', 'not_found');
81
+ throw controlSafetyRefusal('not_found', 'no run in flight to cancel');
77
82
  }
78
83
  try {
79
84
  return await this.client.runs.cancel(this.taskId, opts?.signal !== undefined ? { signal: opts.signal } : undefined);
80
85
  }
81
86
  catch (e) {
82
87
  if (cancelConflictFromError(e)?.kind === 'run_not_running') {
83
- throw new ControlSafetyError(`runs.cancel 409 conflict.run_not_running — ${cancelConflictContent({ kind: 'run_not_running' })}`, 'cancel_run_not_running', e);
88
+ throw controlSafetyRefusal('cancel_run_not_running', 'runs.cancel 409 conflict.run_not_running', cancelConflictContent({ kind: 'run_not_running' }), e);
84
89
  }
85
90
  if (isCancelSuspendedConflict(e)) {
86
- throw new ControlSafetyError('runs.cancel 409 — the pending gate was decided or expired concurrently (lost CAS race); ' +
87
- 'check what that decision did to the run before retrying the cancel', 'cancel_lost_race', e);
91
+ throw controlSafetyRefusal('cancel_lost_race', 'runs.cancel 409', 'the pending gate was decided or expired concurrently (lost CAS race); ' +
92
+ 'check what that decision did to the run before retrying the cancel', e);
88
93
  }
89
94
  if (isNotFound(e)) {
90
- throw new ControlSafetyError('runs.cancel 404 — non-owner / unknown run (no existence leak)', 'not_found', e);
95
+ throw controlSafetyRefusal('not_found', 'runs.cancel 404 — non-owner / unknown run (no existence leak)', undefined, e);
91
96
  }
92
97
  throw e;
93
98
  }
@@ -149,17 +154,17 @@ const STEERING_NAME_TO_SAFETY = new Map([
149
154
  ['SteeringLedgerUnavailableError', 'ledger_unavailable'],
150
155
  ['SteeringError', 'steering_other'],
151
156
  ]);
152
- const STEERING_ADVICE = new Map([
153
- ['not_running', 'the run is suspended/terminal/on another replica — resolve its gate via decide, do not steer'],
154
- ['invalid_content', 'the steer text carried a control-plane escape; surface it, do not strip-and-retry'],
155
- ['queue_full', 'the parked steering queue on this checkpoint is full (core fails loud rather than evicting an accepted instruction) — wait for it to drain, do not resend'],
156
- ['duplicate_input_id', 'this Idempotency-Key is already parked with DIFFERENT steering content — reissue with a fresh key, resending verbatim will never succeed'],
157
- ['ambiguous_target', 'more than one live target matches — address it uniquely (sub-agent: parentToolCallId; workflow: a more specific label); retrying verbatim yields the same result'],
158
- ['still_running', 'the target is STILL RUNNING — steer it instead of resuming, or wait for it to settle (this is the exact opposite of not_running: never collapse the two)'],
159
- ['blocked_by_hook', "this deployment's userPromptSubmit gate refused the input (block, timeout and crash all report this one code, fail-closed) — the input was NOT accepted: no human_input frame, the inputId is not on the ledger, so editing the text and sending again is a normal retry, not a duplicate"],
160
- ['ledger_unavailable', 'the server could not record this steer in its user-input ledger and refused it before anything reached the engine — the input was NOT delivered; resend the same text later (the same Idempotency-Key is fine)'],
161
- ['steering_other', 'an unrecognized steering.* refusal (open set) — the steer did NOT land; log the errorCode verbatim, do not guess its meaning'],
162
- ]);
157
+ const STEERING_ADVICE = {
158
+ not_running: 'the run is suspended/terminal/on another replica — resolve its gate via decide, do not steer',
159
+ invalid_content: 'the steer text carried a control-plane escape; surface it, do not strip-and-retry',
160
+ queue_full: 'the parked steering queue on this checkpoint is full (core fails loud rather than evicting an accepted instruction) — wait for it to drain, do not resend',
161
+ duplicate_input_id: 'this Idempotency-Key is already parked with DIFFERENT steering content — reissue with a fresh key, resending verbatim will never succeed',
162
+ ambiguous_target: 'more than one live target matches — address it uniquely (sub-agent: parentToolCallId; workflow: a more specific label); retrying verbatim yields the same result',
163
+ still_running: 'the target is STILL RUNNING — steer it instead of resuming, or wait for it to settle (this is the exact opposite of not_running: never collapse the two)',
164
+ blocked_by_hook: "this deployment's userPromptSubmit gate refused the input (block, timeout and crash all report this one code, fail-closed) — the input was NOT accepted: no human_input frame, the inputId is not on the ledger, so editing the text and sending again is a normal retry, not a duplicate",
165
+ ledger_unavailable: 'the server could not record this steer in its user-input ledger and refused it before anything reached the engine — the input was NOT delivered; resend the same text later (the same Idempotency-Key is fine)',
166
+ steering_other: 'an unrecognized steering.* refusal (open set) — the steer did NOT land; log the errorCode verbatim, do not guess its meaning',
167
+ };
163
168
  const STEERING_OTHER_UNKNOWN_ADVICE = 'an unrecognized steering.* refusal (open set) — treat delivery as unknown (do not resend as if it had certainly been dropped); log the errorCode verbatim, do not guess its meaning';
164
169
  export function steeringStatusIs4xx(e) {
165
170
  let status;
@@ -172,9 +177,9 @@ export function steeringStatusIs4xx(e) {
172
177
  return status !== undefined && status >= 400 && status < 500;
173
178
  }
174
179
  function steeringAdviceOf(e, safety) {
175
- if (safety === 'steering_other' && !(typeof e === 'object' && e !== null && steeringStatusIs4xx(e)))
180
+ if (safety === 'steering_other' && !steeringStatusIs4xx(e))
176
181
  return STEERING_OTHER_UNKNOWN_ADVICE;
177
- return STEERING_ADVICE.get(safety);
182
+ return STEERING_ADVICE[safety];
178
183
  }
179
184
  const STEERING_REFUSED_BEFORE_ENGINE = new Map([['ledger_unavailable', 503]]);
180
185
  export function steeringRefusedBeforeEngine(e) {
@@ -185,8 +190,7 @@ export function steeringRefusedBeforeEngine(e) {
185
190
  const safety = steeringVerdict(e);
186
191
  if (safety === undefined || STEERING_REFUSED_BEFORE_ENGINE.get(safety) !== status)
187
192
  return undefined;
188
- const advice = STEERING_ADVICE.get(safety);
189
- return advice === undefined ? undefined : { safety, advice };
193
+ return { safety, advice: STEERING_ADVICE[safety] };
190
194
  }
191
195
  catch {
192
196
  return undefined;
@@ -195,7 +199,7 @@ export function steeringRefusedBeforeEngine(e) {
195
199
  function steeringSafetyOf(e, verdict) {
196
200
  if (!STEERING_REFUSED_BEFORE_ENGINE.has(verdict))
197
201
  return verdict;
198
- const refused = typeof e === 'object' && e !== null ? steeringRefusedBeforeEngine(e) : undefined;
202
+ const refused = steeringRefusedBeforeEngine(e);
199
203
  return refused !== undefined && refused.safety === verdict ? verdict : 'steering_other';
200
204
  }
201
205
  function steeringVerdict(e) {
@@ -207,6 +211,40 @@ function steeringVerdict(e) {
207
211
  return STEERING_NAME_TO_SAFETY.get(name);
208
212
  return undefined;
209
213
  }
214
+ export function steerRefusalAdviceOf(e) {
215
+ try {
216
+ const verdict = steeringVerdict(e);
217
+ if (verdict === undefined)
218
+ return undefined;
219
+ const safety = steeringSafetyOf(e, verdict);
220
+ return { safety, advice: steeringAdviceOf(e, safety) };
221
+ }
222
+ catch {
223
+ return undefined;
224
+ }
225
+ }
226
+ function steerRefusalWireLabel(e) {
227
+ try {
228
+ const { code, name } = errCodes(e);
229
+ return code ?? name ?? 'steering.<unknown>';
230
+ }
231
+ catch {
232
+ return 'steering.<unknown>';
233
+ }
234
+ }
235
+ function statusOfThrown(e) {
236
+ try {
237
+ return errCodes(e).status;
238
+ }
239
+ catch {
240
+ return undefined;
241
+ }
242
+ }
243
+ function controlSafetyRefusal(code, head, advice, cause) {
244
+ const status = statusOfThrown(cause);
245
+ const detail = { ...(advice !== undefined ? { advice } : {}), ...(status !== undefined ? { status } : {}) };
246
+ return new ControlSafetyError(advice === undefined ? head : `${head} — ${advice}`, code, cause, detail);
247
+ }
210
248
  function isCancelSuspendedConflict(e) {
211
249
  const { name, status } = errCodes(e);
212
250
  return name === 'ConflictError' || status === 409;
@@ -16,6 +16,7 @@ export interface AllowDenyPolicyOptionsView {
16
16
  deny?: string[];
17
17
  onInvalidName?: 'throw' | 'skip';
18
18
  onInvalidNameIssue?: (issue: AllowDenyInvalidNameIssueView) => void;
19
+ scope?: 'grant' | 'narrowing';
19
20
  }
20
21
  export interface AllowDenyInvalidNameIssueView {
21
22
  entry: string;
@@ -1,5 +1,5 @@
1
1
  import { corruptStoredRowContent, corruptStoredRowWhere } from './wireErrorTriage.js';
2
- import { parkedResumeStartupContent, parkedResumeStartupFromError, resumePolicyBlockContent, resumePolicyBlockFromError, } from './resumeRefusalCopy.js';
2
+ import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, parkedResumeStartupContent, parkedResumeStartupFromError, resumePolicyBlockContent, resumePolicyBlockFromError, } from './resumeRefusalCopy.js';
3
3
  import { decideParkMovedContent, decideParkMovedFromError } from './decideReceipt.js';
4
4
  import { settingsPermissionBucketRefusalContent, settingsPermissionBucketRefusalFromError } from './wireRefusalCopy.js';
5
5
  export function decideFailureNote(err, decision) {
@@ -8,9 +8,11 @@ export function decideFailureNote(err, decision) {
8
8
  const startup = parkedResumeStartupFromError(err, decision);
9
9
  const moved = decideParkMovedFromError(err);
10
10
  const bucket = settingsPermissionBucketRefusalFromError(err);
11
+ const unsupported = checkpointUnsupportedVersionFromError(err);
11
12
  return ((where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`) +
12
13
  (fold === null ? '' : ` — ${resumePolicyBlockContent(fold)}`) +
13
14
  (bucket === null ? '' : ` — ${settingsPermissionBucketRefusalContent(bucket)}`) +
14
15
  (startup === null ? '' : ` — ${parkedResumeStartupContent(startup)}`) +
15
- (moved === null ? '' : ` — ${decideParkMovedContent(moved)}`));
16
+ (moved === null ? '' : ` — ${decideParkMovedContent(moved)}`) +
17
+ (unsupported === null ? '' : ` — ${checkpointUnsupportedVersionContent(unsupported)}`));
16
18
  }
@@ -14,4 +14,5 @@ export type DisableSkillsDeclaration = {
14
14
  };
15
15
  };
16
16
  export declare function disableSkillsDeclaration(want: unknown, baseUrl: string | undefined): DisableSkillsDeclaration;
17
+ export declare function disableSkillsDeclarationSettled(want: boolean | null | undefined, baseUrl: string | undefined, withinMs: number): Promise<DisableSkillsDeclaration>;
17
18
  export {};
@@ -1,4 +1,4 @@
1
- import { engineCapState } from './engineCapsCache.js';
1
+ import { engineCapState, engineCapsSettled } from './engineCapsCache.js';
2
2
  const KEY = 'disableSkills';
3
3
  const shapeOf = (v) => {
4
4
  try {
@@ -50,3 +50,10 @@ export function disableSkillsDeclaration(want, baseUrl) {
50
50
  const cause = WITHHELD_CAUSE_OF[state];
51
51
  return Object.freeze({ field: Object.freeze({}), withheld: Object.freeze({ cause, text: WITHHELD_TEXT[cause] }) });
52
52
  }
53
+ export async function disableSkillsDeclarationSettled(want, baseUrl, withinMs) {
54
+ refuseBadDisableSkills(want, 'disableSkillsDeclarationSettled');
55
+ if (want !== true)
56
+ return disableSkillsDeclaration(want, baseUrl);
57
+ await engineCapsSettled(baseUrl, { withinMs: typeof withinMs === 'number' ? withinMs : 0 });
58
+ return disableSkillsDeclaration(want, baseUrl);
59
+ }