@sema-agent/client-core 0.88.0 → 0.90.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 (61) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +27 -21
  3. package/dist/adapter/activeRunSelfHeal.js +4 -2
  4. package/dist/adapter/downstream/eventToSdkMessage.js +4 -2
  5. package/dist/adapter/downstream/wiringManifestView.d.ts +9 -0
  6. package/dist/adapter/downstream/wiringManifestView.js +27 -0
  7. package/dist/adapter/runStream.d.ts +2 -0
  8. package/dist/adapter/runStream.js +157 -32
  9. package/dist/attachmentsWireCaps.d.ts +13 -2
  10. package/dist/attachmentsWireCaps.js +25 -5
  11. package/dist/controlRouter.d.ts +7 -1
  12. package/dist/controlRouter.js +43 -1
  13. package/dist/coreValuePorts.d.ts +1 -0
  14. package/dist/disableSkillsWire.d.ts +17 -0
  15. package/dist/disableSkillsWire.js +52 -0
  16. package/dist/displayUntrusted.js +122 -55
  17. package/dist/engineCapReader.js +12 -1
  18. package/dist/engineNoticeCodes.d.ts +25 -9
  19. package/dist/engineNoticeCodes.js +161 -68
  20. package/dist/fileHistoryLegNotice.d.ts +20 -0
  21. package/dist/fileHistoryLegNotice.js +122 -0
  22. package/dist/gateOutcome.d.ts +1 -1
  23. package/dist/gateOutcome.js +1 -0
  24. package/dist/generated/engineFactTables.d.ts +1 -0
  25. package/dist/generated/engineFactTables.js +3 -0
  26. package/dist/generated/engineNoticeTables.d.ts +3 -0
  27. package/dist/generated/engineNoticeTables.js +107 -2
  28. package/dist/hitl/toolApprovalWire.d.ts +6 -2
  29. package/dist/hitl/toolApprovalWire.js +30 -0
  30. package/dist/hookToolAliases.d.ts +3 -0
  31. package/dist/hookToolAliases.js +10 -0
  32. package/dist/hooksWireCaps.js +80 -12
  33. package/dist/index.d.ts +14 -3
  34. package/dist/index.js +10 -3
  35. package/dist/permissionsAllowGrantCapability.d.ts +17 -0
  36. package/dist/permissionsAllowGrantCapability.js +75 -0
  37. package/dist/permissionsDeclaration.d.ts +25 -0
  38. package/dist/permissionsDeclaration.js +227 -0
  39. package/dist/pluginHooksWire.d.ts +46 -4
  40. package/dist/pluginHooksWire.js +277 -24
  41. package/dist/promptDelivery.d.ts +5 -2
  42. package/dist/promptDelivery.js +8 -3
  43. package/dist/providerRejection.d.ts +7 -0
  44. package/dist/providerRejection.js +58 -0
  45. package/dist/request/taskRequest.d.ts +2 -0
  46. package/dist/request/taskRequest.js +67 -9
  47. package/dist/runTerminal.d.ts +6 -0
  48. package/dist/runTerminal.js +23 -0
  49. package/dist/sdkRegistryTransit.d.ts +1 -0
  50. package/dist/sdkRegistryTransit.js +1 -0
  51. package/dist/seam.d.ts +3 -0
  52. package/dist/seatContract.d.ts +9 -1
  53. package/dist/seatContract.js +10 -0
  54. package/dist/subagent/engineSubagentSteer.d.ts +8 -1
  55. package/dist/subagent/engineSubagentSteer.js +26 -3
  56. package/dist/supportFloor.d.ts +10 -0
  57. package/dist/supportFloor.js +42 -0
  58. package/dist/taskRequestWords.d.ts +1 -0
  59. package/dist/taskRequestWords.js +14 -0
  60. package/docs/INTEGRATION-CLIENTS.md +896 -14
  61. package/package.json +3 -3
@@ -25,6 +25,12 @@ export const PLUGIN_HOOK_REASONS = [
25
25
  'shell_form_user_config',
26
26
  'user_config_unset',
27
27
  'malformed_entry',
28
+ 'engine_hooks_off',
29
+ 'engine_hooks_unknown',
30
+ 'engine_type_skipped',
31
+ 'engine_llm_limit',
32
+ 'engine_unconfirmed',
33
+ 'engine_elsewhere',
28
34
  ];
29
35
  export const PLUGIN_HOOK_EXCLUSION_REASONS = [
30
36
  'workspace_untrusted',
@@ -32,15 +38,69 @@ export const PLUGIN_HOOK_EXCLUSION_REASONS = [
32
38
  'policy_disable_all',
33
39
  'startup_mode',
34
40
  'managed_plugins_only',
41
+ 'prompt_not_user_authored',
35
42
  ];
36
43
  export const SETTINGS_HOOK_DROP_REASONS = ['exec_form_unsupported', 'mcp_tool_unsupported'];
37
- export const ENGINE_HOOK_LIMITS = { maxGroupsPerEvent: 16, maxEntriesPerEvent: 32, maxCommandChars: 8192 };
44
+ export const ENGINE_HOOK_LIMITS = { maxGroupsPerEvent: 16, maxEntriesPerEvent: 32, maxCommandChars: 8192, maxLlmEntriesPerEvent: 4 };
45
+ const ENGINE_HOOK_TYPE_RUNS = new Set(['command', 'http', 'prompt', 'agent']);
46
+ const COMPACTION_EVENTS = new Set(['PreCompact', 'PostCompact']);
47
+ const judged = (disposition, reason) => ({ kind: 'judged', disposition, reason });
48
+ function hookExecutorOf(x) {
49
+ if (x.event === 'UserPromptSubmit' && !x.promptAuthoredByUser)
50
+ return { kind: 'excluded', reason: 'prompt_not_user_authored' };
51
+ if (x.source === 'settings') {
52
+ const why = x.powershell
53
+ ? { reason: 'engine_type_skipped', certain: true }
54
+ : x.engine.hooksCapable === false
55
+ ? { reason: 'engine_hooks_off', certain: true }
56
+ : x.engine.hooksCapable === null
57
+ ? { reason: 'engine_hooks_unknown', certain: false }
58
+ : x.llmPosition > ENGINE_HOOK_LIMITS.maxLlmEntriesPerEvent
59
+ ? { reason: 'engine_llm_limit', certain: false }
60
+ : undefined;
61
+ const compaction = COMPACTION_EVENTS.has(x.event);
62
+ if (why === undefined) {
63
+ if (!x.shellRuns || x.engine.ownedByThisShell === true)
64
+ return judged('engine', 'projected');
65
+ return compaction ? judged('engine', 'engine_unconfirmed') : judged('shell', 'engine_elsewhere');
66
+ }
67
+ if (x.shellRuns && (why.certain || !compaction || why.reason === 'engine_llm_limit'))
68
+ return judged('shell', why.reason);
69
+ return why.certain ? judged('not_run', why.reason) : judged('engine', 'engine_unconfirmed');
70
+ }
71
+ const why = x.engine.ownedByThisShell === null
72
+ ? 'engine_locality_unknown'
73
+ : !x.engine.ownedByThisShell
74
+ ? 'engine_not_local'
75
+ : x.engine.pluginHooksCapable === null
76
+ ? 'engine_capability_unknown'
77
+ : !x.engine.pluginHooksCapable
78
+ ? 'engine_capability_absent'
79
+ : x.engine.hooksCapable === false
80
+ ? 'engine_hooks_off'
81
+ : x.engine.hooksCapable === null
82
+ ? 'engine_hooks_unknown'
83
+ : x.pluginBlock === 'not_command'
84
+ ? 'not_command'
85
+ : x.powershell
86
+ ? 'engine_type_skipped'
87
+ : x.pluginBlock;
88
+ if (why === undefined)
89
+ return judged('engine', 'projected');
90
+ return judged(x.shellRuns ? 'shell' : 'not_run', why);
91
+ }
38
92
  const ENGINE_EVENTS = new Set(ENGINE_FIRED_HOOK_EVENTS);
39
93
  const USER_CONFIG_REF_ONE = /\$\{user_config\.[^}]+\}/;
40
94
  const USER_CONFIG_REF_ALL = /\$\{user_config\.([^}]+)\}/g;
41
95
  function isRecord(v) {
42
96
  return typeof v === 'object' && v !== null && !Array.isArray(v);
43
97
  }
98
+ function ownField(o, key) {
99
+ return Object.hasOwn(o, key) ? o[key] : undefined;
100
+ }
101
+ function ownView(v) {
102
+ return isRecord(v) ? Object.assign(Object.create(null), Object.fromEntries(Object.entries(v))) : v;
103
+ }
44
104
  function nonEmptyString(v) {
45
105
  return typeof v === 'string' && v.length > 0;
46
106
  }
@@ -123,7 +183,7 @@ export function decidePluginHooks(input) {
123
183
  const excluded = [];
124
184
  const candidates = new Map();
125
185
  let unreadable = 0;
126
- const fallback = (event, reason) => input.shellHookEvents.has(event) ? { disposition: 'shell', reason } : { disposition: 'not_run', reason };
186
+ const fallback = (event, type, reason) => input.shellRuns(event, typeof type === 'string' ? type : undefined) ? { disposition: 'shell', reason } : { disposition: 'not_run', reason };
127
187
  input.plugins.forEach((p, pluginIndex) => {
128
188
  if (!isRecord(p) || !isRecord(p.hooks)) {
129
189
  unreadable++;
@@ -155,6 +215,7 @@ export function decidePluginHooks(input) {
155
215
  excluded.push({ ...key, reason: 'managed_plugins_only' });
156
216
  return;
157
217
  }
218
+ const entryType = isRecord(entry) && typeof entry.type === 'string' ? entry.type : undefined;
158
219
  const shape = sourceOk ? classifyEntry(entry, src.options) : { kind: 'malformed' };
159
220
  if (shape.kind === 'malformed') {
160
221
  verdicts.push({ ...key, disposition: 'not_run', reason: 'malformed_entry' });
@@ -171,27 +232,28 @@ export function decidePluginHooks(input) {
171
232
  if (!ENGINE_EVENTS.has(event)) {
172
233
  verdicts.push({
173
234
  ...key,
174
- ...(input.shellHookEvents.has(event)
235
+ ...(input.shellRuns(event, entryType)
175
236
  ? { disposition: 'shell', reason: 'shell_event' }
176
237
  : { disposition: 'not_run', reason: 'no_executor' }),
177
238
  });
178
239
  return;
179
240
  }
180
- const blocked = input.engine.ownedByThisShell === null
181
- ? 'engine_locality_unknown'
182
- : !input.engine.ownedByThisShell
183
- ? 'engine_not_local'
184
- : input.engine.pluginHooksCapable === null
185
- ? 'engine_capability_unknown'
186
- : !input.engine.pluginHooksCapable
187
- ? 'engine_capability_absent'
188
- : shape.kind === 'other'
189
- ? 'not_command'
190
- : declaresSensitive(src.options)
191
- ? 'sensitive_options'
192
- : undefined;
193
- if (blocked !== undefined) {
194
- verdicts.push({ ...key, ...fallback(event, blocked) });
241
+ const ex = hookExecutorOf({
242
+ source: 'plugin',
243
+ event,
244
+ powershell: shape.kind !== 'other' && isRecord(entry) && entry.shell === 'powershell',
245
+ llmPosition: 0,
246
+ engine: input.engine,
247
+ shellRuns: input.shellRuns(event, entryType),
248
+ promptAuthoredByUser: input.promptAuthoredByUser,
249
+ ...(shape.kind === 'other' ? { pluginBlock: 'not_command' } : declaresSensitive(src.options) ? { pluginBlock: 'sensitive_options' } : {}),
250
+ });
251
+ if (ex.kind === 'excluded') {
252
+ excluded.push({ ...key, reason: ex.reason });
253
+ return;
254
+ }
255
+ if (ex.disposition !== 'engine') {
256
+ verdicts.push({ ...key, disposition: ex.disposition, reason: ex.reason });
195
257
  return;
196
258
  }
197
259
  const plugin = { name: src.name, root: src.root, dataDir: src.dataDir };
@@ -218,7 +280,7 @@ export function decidePluginHooks(input) {
218
280
  const cmd = c.wireEntry.command;
219
281
  if (typeof cmd === 'string' && cmd.length <= ENGINE_HOOK_LIMITS.maxCommandChars)
220
282
  return true;
221
- Object.assign(c.verdict, fallback(event, 'engine_limit'));
283
+ Object.assign(c.verdict, fallback(event, c.wireEntry.type, 'engine_limit'));
222
284
  return false;
223
285
  });
224
286
  if (fits.length === 0)
@@ -233,7 +295,7 @@ export function decidePluginHooks(input) {
233
295
  if (occ.groups + groupKeys.length > ENGINE_HOOK_LIMITS.maxGroupsPerEvent ||
234
296
  occ.entries + fits.length > ENGINE_HOOK_LIMITS.maxEntriesPerEvent) {
235
297
  for (const c of fits)
236
- Object.assign(c.verdict, fallback(event, 'engine_limit'));
298
+ Object.assign(c.verdict, fallback(event, c.wireEntry.type, 'engine_limit'));
237
299
  continue;
238
300
  }
239
301
  const groupsOut = [];
@@ -259,12 +321,13 @@ function carriesArgs(entry) {
259
321
  }
260
322
  return true;
261
323
  }
262
- export function filterSettingsHookGroups(source, event, groups, execFormCapable, hostFires, failLoudMcpTool) {
324
+ export function filterSettingsHookGroups(source, event, groups, execFormCapable, shellRunsType, failLoudMcpTool, judge) {
263
325
  const drops = [];
326
+ const verdicts = [];
327
+ const excluded = [];
264
328
  const out = [];
265
329
  let stripped = 0;
266
330
  const engineEvent = ENGINE_EVENTS.has(event);
267
- const notRun = engineEvent && !hostFires;
268
331
  const keepMcpTool = failLoudMcpTool && engineEvent;
269
332
  groups.forEach((group, groupIndex) => {
270
333
  if (!isRecord(group) || !Array.isArray(group.hooks)) {
@@ -284,10 +347,23 @@ export function filterSettingsHookGroups(source, event, groups, execFormCapable,
284
347
  ? 'exec_form_unsupported'
285
348
  : undefined;
286
349
  if (reason !== undefined) {
287
- drops.push({ source, event, groupIndex, hookIndex, reason, notRun });
350
+ drops.push({ source, event, groupIndex, hookIndex, reason, notRun: engineEvent && !shellRunsType(isRecord(entry) && typeof entry.type === 'string' ? entry.type : undefined) });
288
351
  changed = true;
289
352
  return;
290
353
  }
354
+ if (judge !== undefined && engineEvent && isRecord(entry) && judgeableSettingsEntry(entry)) {
355
+ const ex = judge(event, group, entry);
356
+ if (ex.kind === 'excluded') {
357
+ excluded.push({ source, event, groupIndex, hookIndex, reason: ex.reason });
358
+ changed = true;
359
+ return;
360
+ }
361
+ verdicts.push({ source, event, groupIndex, hookIndex, disposition: ex.disposition, reason: ex.reason });
362
+ if (ex.disposition === 'shell') {
363
+ changed = true;
364
+ return;
365
+ }
366
+ }
291
367
  if (isRecord(entry) && Object.hasOwn(entry, 'plugin')) {
292
368
  const copy = { ...entry };
293
369
  delete copy.plugin;
@@ -303,7 +379,51 @@ export function filterSettingsHookGroups(source, event, groups, execFormCapable,
303
379
  else if (kept.length > 0)
304
380
  out.push({ ...group, hooks: kept });
305
381
  });
306
- return { groups: out, drops, stripped };
382
+ return { groups: out, drops, stripped, verdicts, excluded };
383
+ }
384
+ function judgeableSettingsEntry(entry) {
385
+ const type = entry.type;
386
+ if (typeof type !== 'string' || !ENGINE_HOOK_TYPE_RUNS.has(type))
387
+ return false;
388
+ const field = type === 'command' ? entry.command : type === 'http' ? entry.url : entry.prompt;
389
+ return nonEmptyString(field) && !field.includes('\u0000');
390
+ }
391
+ export function createSettingsHookJudge(engine, shellRuns, promptAuthoredByUser) {
392
+ const llm = new Map();
393
+ const seen = new Map();
394
+ return (event, group, entry) => {
395
+ let dk;
396
+ const entryKey = settingsHookEntryKey(entry);
397
+ if (entryKey !== undefined && isRecord(group)) {
398
+ try {
399
+ const { hooks: _hooks, ...rest } = group;
400
+ void _hooks;
401
+ dk = `${event}\u0000${JSON.stringify(rest)}\u0000${entryKey}`;
402
+ }
403
+ catch {
404
+ dk = undefined;
405
+ }
406
+ }
407
+ const prior = dk === undefined ? undefined : seen.get(dk);
408
+ if (prior !== undefined)
409
+ return prior;
410
+ const type = entry.type;
411
+ const isLlm = type === 'prompt' || type === 'agent';
412
+ const ex = hookExecutorOf({
413
+ source: 'settings',
414
+ event,
415
+ powershell: type === 'command' && entry.shell === 'powershell',
416
+ llmPosition: isLlm ? (llm.get(event) ?? 0) + 1 : 0,
417
+ engine,
418
+ shellRuns: shellRuns(event, typeof type === 'string' ? type : undefined),
419
+ promptAuthoredByUser,
420
+ });
421
+ if (isLlm && ex.kind === 'judged' && ex.disposition !== 'shell')
422
+ llm.set(event, (llm.get(event) ?? 0) + 1);
423
+ if (dk !== undefined)
424
+ seen.set(dk, ex);
425
+ return ex;
426
+ };
307
427
  }
308
428
  function settingsHookEntryKey(entry) {
309
429
  if (!isRecord(entry) || typeof entry.type !== 'string')
@@ -427,6 +547,116 @@ export function dedupeSettingsHookGroups(groups) {
427
547
  export function pluginHookVerdictOf(plan, pluginId, event, groupIndex, hookIndex) {
428
548
  return plan.verdicts.find((v) => v.pluginId === pluginId && v.event === event && v.groupIndex === groupIndex && v.hookIndex === hookIndex);
429
549
  }
550
+ function skipMatcherKey(matcher) {
551
+ return matcher === undefined || matcher === null || matcher === '' || matcher === '*' ? 'null' : JSON.stringify(matcher);
552
+ }
553
+ export function settingsHooksKeptLocally(plan, event, group, opts) {
554
+ let raw;
555
+ try {
556
+ raw = group?.hooks;
557
+ }
558
+ catch {
559
+ raw = undefined;
560
+ }
561
+ const hooks = raw;
562
+ if (!Array.isArray(hooks) || hooks.length === 0 || !ENGINE_EVENTS.has(event))
563
+ return hooks;
564
+ try {
565
+ if (COMPACTION_EVENTS.has(event) && opts?.engineCompaction === 'not_performed')
566
+ return hooks;
567
+ const planRec = plan;
568
+ if (!isRecord(planRec))
569
+ return hooks;
570
+ const excludedList = Object.hasOwn(planRec, 'settingsExcluded') ? planRec.settingsExcluded : [];
571
+ const wire = ownField(planRec, 'wire');
572
+ if (!Array.isArray(excludedList) || (wire !== undefined && !isRecord(wire)))
573
+ return hooks;
574
+ if (excludedList.some((e) => settingsExclusionRowOf(e)?.event === event))
575
+ return [];
576
+ const sent = new Set();
577
+ const groups = isRecord(wire) ? ownField(wire, event) : undefined;
578
+ if (Array.isArray(groups)) {
579
+ for (const g of groups) {
580
+ if (!isRecord(g))
581
+ continue;
582
+ const gh = ownField(g, 'hooks');
583
+ if (!Array.isArray(gh))
584
+ continue;
585
+ const mk = skipMatcherKey(ownField(g, 'matcher'));
586
+ for (const h of gh) {
587
+ if (isRecord(h) && Object.hasOwn(h, 'plugin'))
588
+ continue;
589
+ const k = settingsHookEntryKey(ownView(h));
590
+ if (k !== undefined)
591
+ sent.add(`${mk}\u0000${k}`);
592
+ }
593
+ }
594
+ }
595
+ if (sent.size === 0)
596
+ return hooks;
597
+ const mk = skipMatcherKey(isRecord(group) ? group.matcher : undefined);
598
+ let kept = null;
599
+ hooks.forEach((h, i) => {
600
+ const k = settingsHookEntryKey(h);
601
+ const skip = k !== undefined && sent.has(`${mk}\u0000${k}`);
602
+ if (skip)
603
+ kept ??= hooks.slice(0, i);
604
+ else if (kept !== null)
605
+ kept.push(h);
606
+ });
607
+ return kept ?? hooks;
608
+ }
609
+ catch {
610
+ return hooks;
611
+ }
612
+ }
613
+ function settingsRowBaseOf(v) {
614
+ if (!isRecord(v))
615
+ return undefined;
616
+ const source = ownField(v, 'source');
617
+ const event = ownField(v, 'event');
618
+ const groupIndex = ownField(v, 'groupIndex');
619
+ const hookIndex = ownField(v, 'hookIndex');
620
+ if (typeof source !== 'string' || !Object.hasOwn(SOURCE_TEXT, source))
621
+ return undefined;
622
+ if (typeof event !== 'string' || !ENGINE_EVENTS.has(event))
623
+ return undefined;
624
+ if (!Number.isInteger(groupIndex) || groupIndex < 0 || !Number.isInteger(hookIndex) || hookIndex < 0)
625
+ return undefined;
626
+ return { source: source, event, groupIndex: groupIndex, hookIndex: hookIndex, o: v };
627
+ }
628
+ const DISPOSITION_SET = new Set(PLUGIN_HOOK_DISPOSITIONS);
629
+ const REASON_SET = new Set(PLUGIN_HOOK_REASONS);
630
+ const EXCLUSION_REASON_SET = new Set(PLUGIN_HOOK_EXCLUSION_REASONS);
631
+ function settingsVerdictRowOf(v) {
632
+ try {
633
+ const b = settingsRowBaseOf(v);
634
+ if (b === undefined)
635
+ return undefined;
636
+ const disposition = ownField(b.o, 'disposition');
637
+ const reason = ownField(b.o, 'reason');
638
+ if (typeof disposition !== 'string' || !DISPOSITION_SET.has(disposition) || typeof reason !== 'string' || !REASON_SET.has(reason))
639
+ return undefined;
640
+ return { source: b.source, event: b.event, groupIndex: b.groupIndex, hookIndex: b.hookIndex, disposition: disposition, reason: reason };
641
+ }
642
+ catch {
643
+ return undefined;
644
+ }
645
+ }
646
+ function settingsExclusionRowOf(v) {
647
+ try {
648
+ const b = settingsRowBaseOf(v);
649
+ if (b === undefined)
650
+ return undefined;
651
+ const reason = ownField(b.o, 'reason');
652
+ if (typeof reason !== 'string' || !EXCLUSION_REASON_SET.has(reason))
653
+ return undefined;
654
+ return { source: b.source, event: b.event, groupIndex: b.groupIndex, hookIndex: b.hookIndex, reason: reason };
655
+ }
656
+ catch {
657
+ return undefined;
658
+ }
659
+ }
430
660
  const REASON_TEXT = {
431
661
  projected: 'it is sent to the engine',
432
662
  shell_event: 'this client runs this event itself',
@@ -441,6 +671,12 @@ const REASON_TEXT = {
441
671
  shell_form_user_config: 'its command uses ${user_config.…} in shell form, which is refused (use exec form with "args", or read $CLAUDE_PLUGIN_OPTION_<KEY>)',
442
672
  user_config_unset: 'its command uses a plugin option that has not been set',
443
673
  malformed_entry: 'the hook entry is malformed',
674
+ engine_hooks_off: 'the connected engine ignores hooks sent with requests',
675
+ engine_hooks_unknown: 'it is not known whether the connected engine runs hooks sent with requests',
676
+ engine_type_skipped: 'the engine does not run PowerShell command hooks',
677
+ engine_llm_limit: 'the engine may skip prompt or agent hooks after the first four on one event',
678
+ engine_unconfirmed: 'it is sent to the engine, which is expected to run it, but this client cannot confirm that',
679
+ engine_elsewhere: 'it is not confirmed that the engine was started by this client on this machine, so this client runs it itself',
444
680
  };
445
681
  const DROP_TEXT = {
446
682
  exec_form_unsupported: 'exec-form hooks (with "args") are sent to the engine only once it reports support for them',
@@ -545,5 +781,22 @@ export function hooksNotRunNotice(plan) {
545
781
  const events = row.events.map((e) => noticeLabel(e)).join(', ');
546
782
  lines.push(`${SOURCE_TEXT[row.source]}: ${events} ${row.count === 1 ? 'hook' : 'hooks'} will not run — ${settingsHookDropText(row.reason)}.`);
547
783
  }
784
+ const settingsVerdicts = plan.settingsVerdicts;
785
+ const byVerdict = new Map();
786
+ for (const raw of Array.isArray(settingsVerdicts) ? settingsVerdicts : []) {
787
+ const v = settingsVerdictRowOf(raw);
788
+ if (v === undefined || v.disposition !== 'not_run')
789
+ continue;
790
+ const k = `${v.source}\u0000${v.reason}`;
791
+ const row = byVerdict.get(k) ?? { source: v.source, reason: v.reason, events: [], count: 0 };
792
+ if (!row.events.includes(v.event))
793
+ row.events.push(v.event);
794
+ row.count++;
795
+ byVerdict.set(k, row);
796
+ }
797
+ for (const row of byVerdict.values()) {
798
+ const events = row.events.map((e) => noticeLabel(e)).join(', ');
799
+ lines.push(`${SOURCE_TEXT[row.source]}: ${events} ${row.count === 1 ? 'hook' : 'hooks'} will not run — ${pluginHookReasonText(row.reason)}.`);
800
+ }
548
801
  return lines;
549
802
  }
@@ -14,7 +14,10 @@ export interface PromptDeliveryLedger {
14
14
  batchOf(uuid: string): readonly string[] | undefined;
15
15
  toRecords(): readonly PromptDeliveryRecord[];
16
16
  fromRecords(records: readonly PromptDeliveryRecord[]): readonly PromptDeliveryRecord[];
17
+ knownUndeliveredMessage(message: object | null | undefined): boolean;
18
+ withoutUndelivered<T>(messages: readonly T[]): T[];
17
19
  }
20
+ export declare function frameProvesDelivery(event: object | null | undefined): boolean;
18
21
  export declare function createPromptDeliveryLedger(): PromptDeliveryLedger;
19
- export declare function promptKnownUndelivered(ledger: PromptDeliveryLedger, message: object | null | undefined): boolean;
20
- export declare function withoutUndeliveredPrompts<T>(ledger: PromptDeliveryLedger, messages: readonly T[]): T[];
22
+ export declare function promptKnownUndelivered(ledger: Pick<PromptDeliveryLedger, 'knownUndelivered'>, message: object | null | undefined): boolean;
23
+ export declare function withoutUndeliveredPrompts<T>(ledger: Pick<PromptDeliveryLedger, 'knownUndelivered'>, messages: readonly T[]): T[];
@@ -32,10 +32,12 @@ function readRecord(rec) {
32
32
  }
33
33
  }
34
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))
35
+ export function frameProvesDelivery(event) {
36
+ if (typeof event !== 'object' || event === null)
37
37
  return false;
38
38
  try {
39
+ if (Array.isArray(event))
40
+ return false;
39
41
  const type = event.type;
40
42
  if (typeof type !== 'string' || type.length === 0 || PRE_RUN_FRAME_TYPES.has(type))
41
43
  return false;
@@ -83,7 +85,7 @@ export function createPromptDeliveryLedger() {
83
85
  add(u, 'd', added);
84
86
  return Object.freeze(added);
85
87
  };
86
- return Object.freeze({
88
+ const ledger = Object.freeze({
87
89
  attempted(batchUuids) {
88
90
  const batch = Object.freeze(cleanBatch(batchUuids));
89
91
  const added = [];
@@ -122,7 +124,10 @@ export function createPromptDeliveryLedger() {
122
124
  }
123
125
  return Object.freeze(added);
124
126
  },
127
+ knownUndeliveredMessage: (message) => promptKnownUndelivered(ledger, message),
128
+ withoutUndelivered: (messages) => withoutUndeliveredPrompts(ledger, messages),
125
129
  });
130
+ return ledger;
126
131
  }
127
132
  function uuidOfMessage(m) {
128
133
  if (typeof m !== 'object' || m === null)
@@ -0,0 +1,7 @@
1
+ import { type RunTerminalFailureDetail } from './runTerminal.js';
2
+ import { type ToolHistoryErrorCarrier } from './toolHistoryMismatch.js';
3
+ export interface ProviderRejectionResend {
4
+ resend: 'edit_and_resend' | 'not_by_editing';
5
+ text: string;
6
+ }
7
+ export declare function providerRejectionResend(detail: RunTerminalFailureDetail | null | undefined, carrier?: ToolHistoryErrorCarrier | null): ProviderRejectionResend | undefined;
@@ -0,0 +1,58 @@
1
+ import { readRunTerminal } from './runTerminal.js';
2
+ import { classifyToolHistoryMismatch } from './toolHistoryMismatch.js';
3
+ const PROVIDER_STATUS_TIERS = Object.freeze([
4
+ Object.freeze([400, 400, 'invalid_request']),
5
+ Object.freeze([401, 401, 'authentication_failed']),
6
+ Object.freeze([403, 403, 'authentication_failed']),
7
+ Object.freeze([404, 404, 'model_not_found']),
8
+ Object.freeze([413, 413, 'invalid_request']),
9
+ Object.freeze([422, 422, 'invalid_request']),
10
+ Object.freeze([429, 429, 'rate_limit']),
11
+ Object.freeze([500, 599, 'server_error']),
12
+ ]);
13
+ const UNTIERED = 'unknown';
14
+ function providerStatusTier(status) {
15
+ for (const [lo, hi, tier] of PROVIDER_STATUS_TIERS)
16
+ if (status >= lo && status <= hi)
17
+ return tier;
18
+ return UNTIERED;
19
+ }
20
+ const EDITABLE_TIER = 'invalid_request';
21
+ const NOT_BY_EDITING_TAIL = new Map([
22
+ ['authentication_failed', ': check the provider credentials or access for this account.'],
23
+ ['model_not_found', ': the model is not available from this provider — choose another model.'],
24
+ ['rate_limit', ': the provider is rate-limiting this account — wait and try again.'],
25
+ ['server_error', ': the provider reported an internal error — try again later.'],
26
+ [UNTIERED, ' for a reason that editing it will not fix — check the provider account or try again later.'],
27
+ ]);
28
+ function toolHistoryMismatchCarrier(carrier) {
29
+ return carrier !== undefined && carrier !== null && classifyToolHistoryMismatch(carrier) !== null;
30
+ }
31
+ const PROVIDER_REJECTED = 'provider_rejected';
32
+ export function providerRejectionResend(detail, carrier) {
33
+ let read;
34
+ try {
35
+ const r = readRunTerminal({ terminal: { kind: 'failed', detail } });
36
+ read = r !== null && r.kind === 'failed' && Object.hasOwn(r, 'detail') ? r.detail : undefined;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ if (read === undefined || !Object.hasOwn(read, 'kind') || read.kind !== PROVIDER_REJECTED)
42
+ return undefined;
43
+ const status = Object.hasOwn(read, 'status') ? read.status : undefined;
44
+ if (status === undefined)
45
+ return undefined;
46
+ if (toolHistoryMismatchCarrier(carrier))
47
+ return undefined;
48
+ const tier = providerStatusTier(status);
49
+ return tier === EDITABLE_TIER
50
+ ? {
51
+ resend: 'edit_and_resend',
52
+ text: `The model provider rejected this request as sent (HTTP ${status}). Change it — for example shorten it or remove an attachment — and send it again.`,
53
+ }
54
+ : {
55
+ resend: 'not_by_editing',
56
+ text: `The model provider refused this request (HTTP ${status})${NOT_BY_EDITING_TAIL.get(tier) ?? NOT_BY_EDITING_TAIL.get(UNTIERED) ?? ''}`,
57
+ };
58
+ }
@@ -51,6 +51,7 @@ export interface TaskRequestInput {
51
51
  fileHistory?: 'disabled';
52
52
  inheritEnv?: InheritEnvWord;
53
53
  requireExistingSession?: TaskRequest['requireExistingSession'];
54
+ disableSkills?: true;
54
55
  rewind?: Record<string, unknown>;
55
56
  repo?: string;
56
57
  council?: boolean;
@@ -74,6 +75,7 @@ export interface TaskRequestOmission {
74
75
  export declare function taskRequestOmissionCauseDetail(cause: unknown): string;
75
76
  export declare const TASK_REQUEST_OMISSION_CAUSE_NOTICES: Readonly<Record<TaskRequestOmissionCause, string>>;
76
77
  export declare function taskRequestOmissionCauseNotice(cause: unknown): string;
78
+ export declare function taskRequestOmissionCausePhrase(cause: unknown): string;
77
79
  export interface TaskRequestAssembly {
78
80
  readonly request: TaskRequestLike;
79
81
  readonly omitted: readonly TaskRequestOmission[];