@sema-agent/client-core 0.92.0 → 0.93.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 (123) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +23 -16
  3. package/dist/adapt/arms.js +2 -0
  4. package/dist/adapt/toolCards.js +6 -4
  5. package/dist/adapt.d.ts +1 -1
  6. package/dist/adapt.js +1 -1
  7. package/dist/adapter/runStream.js +3 -1
  8. package/dist/agentsWireCaps.js +1 -1
  9. package/dist/approvalDecidedByCapability.d.ts +16 -0
  10. package/dist/approvalDecidedByCapability.js +47 -0
  11. package/dist/approvalsStreamLiveCapability.js +3 -3
  12. package/dist/askSurvivesStandingAnswer.d.ts +1 -1
  13. package/dist/centerWiringCapability.js +1 -1
  14. package/dist/cloudConfigWireCaps.js +5 -5
  15. package/dist/compensations.js +2 -2
  16. package/dist/decideReceipt.d.ts +2 -0
  17. package/dist/decideReceipt.js +34 -9
  18. package/dist/detachWire.d.ts +2 -14
  19. package/dist/detachWire.js +1 -47
  20. package/dist/deviceExecutorManagementCapability.d.ts +6 -2
  21. package/dist/deviceExecutorManagementCapability.js +61 -15
  22. package/dist/engineConfigDrift.d.ts +20 -0
  23. package/dist/engineConfigDrift.js +54 -0
  24. package/dist/engineErrorCodes.d.ts +3 -0
  25. package/dist/engineErrorCodes.js +12 -4
  26. package/dist/engineInlineTaskStats.d.ts +1 -0
  27. package/dist/engineNoticeCodes.d.ts +2 -2
  28. package/dist/engineWireFor.d.ts +22 -0
  29. package/dist/engineWireFor.js +154 -9
  30. package/dist/engineWireSdk.d.ts +0 -1
  31. package/dist/engineWireSdk.js +0 -3
  32. package/dist/executionLaneCapability.js +2 -2
  33. package/dist/fileHistoryCaptureCapability.d.ts +1 -1
  34. package/dist/fileHistoryCaptureCapability.js +1 -6
  35. package/dist/fileHistoryLegNotice.d.ts +1 -1
  36. package/dist/fileHistoryLegNotice.js +1 -6
  37. package/dist/gateVocabulary.d.ts +1 -5
  38. package/dist/gateVocabulary.js +4 -22
  39. package/dist/generated/engineFactTables.d.ts +4 -3
  40. package/dist/generated/engineFactTables.js +10 -0
  41. package/dist/generated/engineNoticeTables.d.ts +1 -1
  42. package/dist/generated/engineNoticeTables.js +1 -1
  43. package/dist/goalStopHook.d.ts +1 -8
  44. package/dist/goalStopHook.js +2 -58
  45. package/dist/headlessPermissionModeWire.d.ts +3 -2
  46. package/dist/headlessPermissionModeWire.js +46 -5
  47. package/dist/hitl/askParkRowRouting.js +2 -2
  48. package/dist/hitl/frameRouter.js +7 -2
  49. package/dist/hitl/gateLedger.d.ts +1 -0
  50. package/dist/hitl/gateLedger.js +3 -0
  51. package/dist/hitl/hitlBridge.d.ts +1 -0
  52. package/dist/hitl/hitlBridge.js +6 -1
  53. package/dist/hitl/hitlHostSurface.d.ts +3 -2
  54. package/dist/hitl/hitlHostSurface.js +18 -3
  55. package/dist/hitl/parkResolver.js +22 -4
  56. package/dist/hitl/persistedRulesWire.js +29 -17
  57. package/dist/hitl/planReviewWire.d.ts +0 -2
  58. package/dist/hitl/planReviewWire.js +1 -18
  59. package/dist/hitl/sessionPolicyDeliverable.js +3 -3
  60. package/dist/hitl/sessionPolicyWire.d.ts +2 -0
  61. package/dist/hitl/sessionPolicyWire.js +7 -4
  62. package/dist/hitl/toolApprovalWire.d.ts +22 -7
  63. package/dist/hitl/toolApprovalWire.js +133 -54
  64. package/dist/hooksWireCaps.d.ts +1 -1
  65. package/dist/hooksWireCaps.js +1 -1
  66. package/dist/index.d.ts +7 -2
  67. package/dist/index.js +6 -2
  68. package/dist/inheritEnvWire.d.ts +1 -4
  69. package/dist/inheritEnvWire.js +0 -18
  70. package/dist/limitsWire.d.ts +0 -6
  71. package/dist/limitsWire.js +0 -38
  72. package/dist/mcpProbeCapability.js +1 -1
  73. package/dist/mcpProbeWire.js +10 -12
  74. package/dist/mcpReconnect.d.ts +5 -0
  75. package/dist/mcpReconnect.js +28 -0
  76. package/dist/memoryComplianceCapability.js +2 -2
  77. package/dist/memoryOriginCapability.js +2 -2
  78. package/dist/memoryVerbsWire.js +14 -13
  79. package/dist/ownKey.d.ts +2 -0
  80. package/dist/ownKey.js +15 -0
  81. package/dist/peerLaneCapability.js +2 -2
  82. package/dist/permissionModesCapability.d.ts +16 -0
  83. package/dist/permissionModesCapability.js +71 -0
  84. package/dist/permissionRuleIssue.js +1 -1
  85. package/dist/permissionRulesWriteCapability.js +2 -2
  86. package/dist/permissionWireCaps.d.ts +2 -0
  87. package/dist/permissionWireCaps.js +8 -1
  88. package/dist/permissionsAllowGrantCapability.js +7 -2
  89. package/dist/postureKnob.js +1 -1
  90. package/dist/protocolSegment.d.ts +2 -0
  91. package/dist/protocolSegment.js +3 -0
  92. package/dist/readFacePosture.js +1 -1
  93. package/dist/resumeRefusalCopy.d.ts +1 -0
  94. package/dist/resumeRefusalCopy.js +26 -3
  95. package/dist/retryStatus.js +2 -1
  96. package/dist/rewindArchiveCapability.js +2 -2
  97. package/dist/runTerminal.d.ts +16 -0
  98. package/dist/runTerminal.js +32 -3
  99. package/dist/seam.d.ts +1 -0
  100. package/dist/sessionPolicyCapability.js +1 -1
  101. package/dist/sessionSyncFileHistoryCapability.js +2 -2
  102. package/dist/skillToolListsCapability.js +1 -1
  103. package/dist/sqlEngineCapability.js +1 -1
  104. package/dist/steerPriorityCapability.js +1 -1
  105. package/dist/storePostureCapability.js +2 -2
  106. package/dist/supportFloor.d.ts +1 -0
  107. package/dist/supportFloor.js +13 -3
  108. package/dist/taskRequestWords.d.ts +4 -2
  109. package/dist/taskRequestWords.js +1 -1
  110. package/dist/toolHistoryBadTurn.js +1 -2
  111. package/dist/toolResult.d.ts +2 -1
  112. package/dist/toolResult.js +33 -3
  113. package/dist/userInvokedSkillsCapability.js +1 -1
  114. package/dist/webSearchBackendCapability.js +2 -2
  115. package/dist/webSearchWireCaps.d.ts +1 -1
  116. package/dist/webSearchWireCaps.js +40 -2
  117. package/dist/wireFailureShape.js +2 -2
  118. package/dist/wireRefusalCopy.d.ts +9 -3
  119. package/dist/wireRefusalCopy.js +41 -4
  120. package/dist/workflowsJournalCapability.js +1 -1
  121. package/dist/writeProtectionCapability.js +1 -1
  122. package/docs/INTEGRATION-CLIENTS.md +901 -15
  123. package/package.json +2 -2
@@ -1,80 +1,24 @@
1
1
  import { hostEnv } from './hostEnv.js';
2
2
  import { envFlagOff } from './envFlag.js';
3
- import { engineCapString } from './engineCapsCache.js';
4
3
  let wireSessionStopHookPrompt = null;
5
- let wireSessionStopHookCcSemantics = false;
6
4
  export const GOAL_STOP_HOOK_WIRE_ENV = 'SEMA_GOAL_STOP_HOOK_WIRE';
7
- export const CC_STOP_SEMANTICS_MIN_SERVER = '1.279.3';
8
- function parseSemverCore(v) {
9
- const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
10
- return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
11
- }
12
- export function ccStopSemanticsFromVersion(version) {
13
- if (typeof version !== 'string' || version.length === 0)
14
- return false;
15
- const got = parseSemverCore(version);
16
- const min = parseSemverCore(CC_STOP_SEMANTICS_MIN_SERVER);
17
- if (got === null || min === null)
18
- return false;
19
- const [gotMajor, gotMinor, gotPatch] = got;
20
- const [minMajor, minMinor, minPatch] = min;
21
- if (gotMajor !== minMajor)
22
- return gotMajor > minMajor;
23
- if (gotMinor !== minMinor)
24
- return gotMinor > minMinor;
25
- if (gotPatch !== minPatch)
26
- return gotPatch > minPatch;
27
- return true;
28
- }
29
- export function engineCcStopSemantics(env = hostEnv()) {
30
- const base = env.SEMA_LIVE_BASEURL;
31
- if (!base)
32
- return false;
33
- return ccStopSemanticsFromVersion(engineCapString(base, 'version'));
34
- }
35
5
  export function isGoalStopHookWireArmed(env = hostEnv()) {
36
6
  return !envFlagOff(env[GOAL_STOP_HOOK_WIRE_ENV]);
37
7
  }
38
- export function setWireSessionStopHook(prompt, opts) {
8
+ export function setWireSessionStopHook(prompt) {
39
9
  wireSessionStopHookPrompt = prompt && prompt.length > 0 ? prompt : null;
40
- wireSessionStopHookCcSemantics = wireSessionStopHookPrompt === null ? false : opts?.ccSemantics === true;
41
10
  }
42
11
  export function getWireSessionStopHook() {
43
12
  return wireSessionStopHookPrompt;
44
13
  }
45
- export function getWireSessionStopHookCcSemantics() {
46
- return wireSessionStopHookCcSemantics;
47
- }
48
- export function buildGoalStopHookPrompt(condition) {
49
- const reason = `[session goal] Before you stop: verify that this goal actually holds — ${condition}. ` +
50
- `If it does hold, say so plainly and finish. If it does not, keep working toward it and do not ` +
51
- `pause to ask the user what to do.`;
52
- return ('You are the Stop gate for an autonomous coding agent session. The user set this session goal, ' +
53
- 'which the agent is supposed to satisfy before it stops:\n' +
54
- `<goal>\n${condition}\n</goal>\n` +
55
- 'Stop-hook input JSON: $ARGUMENTS\n' +
56
- 'Reply with RAW JSON only — no prose, no explanation, no code fences.\n' +
57
- 'If the input JSON has "stop_hook_active" set to true, this gate already fired once for this turn: ' +
58
- 'reply with exactly {"continue":true} so the agent may stop.\n' +
59
- 'Otherwise reply with exactly:\n' +
60
- JSON.stringify({ decision: 'block', reason }));
61
- }
62
14
  export function goalStopHookMatcher(managedOnly) {
63
15
  if (wireSessionStopHookPrompt === null || managedOnly || !isGoalStopHookWireArmed())
64
16
  return null;
65
17
  return {
66
18
  matcher: '',
67
- hooks: [
68
- {
69
- type: 'prompt',
70
- prompt: wireSessionStopHookCcSemantics
71
- ? wireSessionStopHookPrompt
72
- : buildGoalStopHookPrompt(wireSessionStopHookPrompt),
73
- },
74
- ],
19
+ hooks: [{ type: 'prompt', prompt: wireSessionStopHookPrompt }],
75
20
  };
76
21
  }
77
22
  export function __resetGoalStopHookForTests() {
78
23
  wireSessionStopHookPrompt = null;
79
- wireSessionStopHookCcSemantics = false;
80
24
  }
@@ -1,13 +1,14 @@
1
1
  import { type PermissionMode, type PermissionModeField } from './permissionWireCaps.js';
2
+ import { type PermissionModesReading } from './permissionModesCapability.js';
2
3
  import { type EnvLike } from './hostEnv.js';
3
4
  export declare const HEADLESS_PERMISSION_MODE_ENV = "SEMA_HEADLESS_PERMISSION_MODE";
4
5
  export declare const HEADLESS_PERMISSION_WIRE_MODES: readonly PermissionMode[];
5
- export declare function normalizeShellPermissionModeForWire(mode: string | undefined): string | undefined;
6
+ export declare function normalizeShellPermissionModeForWire(mode: string | undefined, permissionModes?: PermissionModesReading): string | undefined;
6
7
  export type HeadlessPermissionResolution = {
7
8
  fields: PermissionModeField;
8
9
  source: 'flag' | 'env' | 'default' | 'absent';
9
10
  warning?: string;
10
11
  };
11
12
  export declare function permissionModeFromArgv(argv: readonly string[]): string | undefined;
12
- export declare function resolveHeadlessPermissionMode(argv: readonly string[], env?: EnvLike): HeadlessPermissionResolution;
13
+ export declare function resolveHeadlessPermissionMode(argv: readonly string[], env?: EnvLike, permissionModes?: PermissionModesReading): HeadlessPermissionResolution;
13
14
  export declare function _resetHeadlessPermissionModeWireForTest(): void;
@@ -1,16 +1,20 @@
1
1
  import { permissionModeField } from './permissionWireCaps.js';
2
+ import { permissionModeAdmitted } from './permissionModesCapability.js';
2
3
  import { hostEnv } from './hostEnv.js';
3
4
  import { lastFlagValue } from './argvFlagValue.js';
4
5
  import { PERMISSION_MODE_WORDS } from './taskRequestWords.js';
6
+ import { readOwnDataValue } from './ownKey.js';
5
7
  export const HEADLESS_PERMISSION_MODE_ENV = 'SEMA_HEADLESS_PERMISSION_MODE';
6
8
  export const HEADLESS_PERMISSION_WIRE_MODES = [...PERMISSION_MODE_WORDS];
7
9
  const SHELL_INTERNAL_MODE_TO_WIRE = new Map([
8
10
  ['manual', 'default'],
9
11
  ['dontAsk', 'default'],
10
12
  ]);
11
- export function normalizeShellPermissionModeForWire(mode) {
13
+ export function normalizeShellPermissionModeForWire(mode, permissionModes) {
12
14
  if (mode === undefined)
13
15
  return undefined;
16
+ if (mode === 'dontAsk' && permissionModeAdmitted(permissionModes, mode))
17
+ return mode;
14
18
  return SHELL_INTERNAL_MODE_TO_WIRE.get(mode) ?? mode;
15
19
  }
16
20
  export function permissionModeFromArgv(argv) {
@@ -27,19 +31,32 @@ export function permissionModeFromArgv(argv) {
27
31
  return undefined;
28
32
  }
29
33
  let warnedInvalidEnv = false;
30
- export function resolveHeadlessPermissionMode(argv, env = hostEnv()) {
34
+ export function resolveHeadlessPermissionMode(argv, env = hostEnv(), permissionModes) {
31
35
  const flagMode = permissionModeFromArgv(argv);
32
36
  if (flagMode !== undefined) {
33
- const normalized = normalizeShellPermissionModeForWire(flagMode);
34
- return { fields: permissionModeField(normalized, { stampDefault: true }), source: 'flag' };
37
+ const snap = flagMode === 'dontAsk' ? dontAskSnapshot(permissionModes) : undefined;
38
+ const normalized = normalizeShellPermissionModeForWire(flagMode, snap);
39
+ return { fields: permissionModeField(normalized, { stampDefault: true, permissionModes: snap }), source: 'flag' };
35
40
  }
36
41
  const raw = env[HEADLESS_PERMISSION_MODE_ENV]?.trim();
37
42
  if (raw === undefined || raw === '') {
38
43
  return { fields: permissionModeField('default', { stampDefault: true }), source: 'default' };
39
44
  }
40
45
  if (HEADLESS_PERMISSION_WIRE_MODES.includes(raw)) {
46
+ const snap = raw === 'dontAsk' ? dontAskSnapshot(permissionModes) : undefined;
47
+ if (raw === 'dontAsk' && snap === undefined) {
48
+ const downgraded = {
49
+ fields: permissionModeField('default', { stampDefault: true }),
50
+ source: 'env',
51
+ };
52
+ if (!warnedInvalidEnv) {
53
+ warnedInvalidEnv = true;
54
+ downgraded.warning = `sema: ${HEADLESS_PERMISSION_MODE_ENV}="dontAsk" is only sent once this client knows the engine accepts that mode, and ${dontAskWithheldClause(permissionModes)} — this run uses "default"`;
55
+ }
56
+ return downgraded;
57
+ }
41
58
  return {
42
- fields: permissionModeField(raw, { stampDefault: true }),
59
+ fields: permissionModeField(raw, { stampDefault: true, permissionModes: snap }),
43
60
  source: 'env',
44
61
  };
45
62
  }
@@ -53,6 +70,30 @@ export function resolveHeadlessPermissionMode(argv, env = hostEnv()) {
53
70
  }
54
71
  return resolution;
55
72
  }
73
+ function dontAskSnapshot(reading) {
74
+ return permissionModeAdmitted(reading, 'dontAsk') ? { kind: 'present', modes: Object.freeze(['dontAsk']) } : undefined;
75
+ }
76
+ function dontAskWithheldClause(reading) {
77
+ if (reading === undefined)
78
+ return 'no such reading was supplied for this run';
79
+ let kind;
80
+ try {
81
+ kind = typeof reading === 'object' && reading !== null ? readOwnDataValue(reading, 'kind') : undefined;
82
+ }
83
+ catch {
84
+ kind = undefined;
85
+ }
86
+ switch (kind) {
87
+ case 'unobserved':
88
+ return "this engine's capabilities have not been read yet (wait for them before the first request)";
89
+ case 'not_reported':
90
+ return 'this engine does not report which modes it accepts';
91
+ case 'present':
92
+ return "this engine's list of accepted modes does not include it";
93
+ default:
94
+ return 'the reading supplied for it could not be read';
95
+ }
96
+ }
56
97
  export function _resetHeadlessPermissionModeWireForTest() {
57
98
  warnedInvalidEnv = false;
58
99
  }
@@ -15,9 +15,9 @@ export function classifyAskParkRows(rows, taskId, deps) {
15
15
  kind: 'unborn',
16
16
  reason: rows.length === 0
17
17
  ?
18
- `no pending row at all for task ${taskId} — either an orphan park (a seat held with no decision outstanding) or the next gate's row is not minted yet`
18
+ `no pending row at all for task ${taskId} — either an orphan park (a seat held with no decision outstanding) or the next gate's row is not minted yet; this client cannot tell which`
19
19
  : soleUnowned
20
- ? `the only pending row (task ${String(sole?.taskId)} / session ${String(sole?.sessionId)}) could not be proven to belong to this client (not in its own-run ledger, session id absent or different) — refusing to surface an approval that may be another session's`
20
+ ? `the only pending row (task ${String(sole?.taskId)} / session ${String(sole?.sessionId)}) could not be proven to belong to this client (not in its own-run ledger, session id absent or different; this client cannot tell which) — refusing to surface an approval that may be another session's`
21
21
  : `${String(rows.length)} pending rows but none for task ${taskId} — ambiguous queue, refusing to surface an unrelated row`,
22
22
  };
23
23
  }
@@ -66,6 +66,11 @@ function isGatedToolName(name) {
66
66
  function isFsOrShellToolName(name) {
67
67
  return typeof name === 'string' && (toolNameIsFsWrite(name) || toolNameIsShellExec(name));
68
68
  }
69
+ function parkFamilyEvidence(led, gateToolName, gatedCallId) {
70
+ if (!isFsOrShellToolName(gateToolName))
71
+ return false;
72
+ return gatedCallId === undefined || led.isFsOrShellGatedCall(gatedCallId);
73
+ }
69
74
  const ENGINE_GATE_PARKED_ERROR_CODE = GATE_PARKED_ERROR_CODE;
70
75
  function isEngineAbortToolEnd(ev) {
71
76
  if (ev.errorCode === ENGINE_GATE_PARKED_ERROR_CODE)
@@ -353,7 +358,7 @@ function routeSuspended(ev, ctx) {
353
358
  if (ev.gate &&
354
359
  isToolApprovalGate(ev.gate) &&
355
360
  ctx.taskId.current) {
356
- ctx.led.noteParkGate({ fsOrShellFamily: isFsOrShellToolName(ev.gate.toolName) });
361
+ ctx.led.noteParkGate({ fsOrShellFamily: parkFamilyEvidence(ctx.led, ev.gate.toolName, gatedCallId) });
357
362
  return { kind: 'park', gate: 'fs', ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
358
363
  }
359
364
  return { kind: 'yield', events: [ev] };
@@ -364,7 +369,7 @@ function routeDone(ev, ctx) {
364
369
  const park = parkGateOf(ev.result);
365
370
  if (park !== null) {
366
371
  ctx.taskId.current = ctx.taskId.current ?? park.taskId;
367
- led.noteParkGate({ fsOrShellFamily: park.gate === 'fs' && isFsOrShellToolName(park.toolName) });
372
+ led.noteParkGate({ fsOrShellFamily: park.gate === 'fs' && parkFamilyEvidence(led, park.toolName, gatedCallId) });
368
373
  return { kind: 'park', gate: park.gate, pendingDone: ev, ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
369
374
  }
370
375
  const events = [...flushHeldWithInterruptRewrite(led, { terminal: ev, signal: ctx.signal })];
@@ -46,6 +46,7 @@ export interface GateLedger {
46
46
  lastPendingFsCall(): string | undefined;
47
47
  noteFsOrShellGate(callId: string): void;
48
48
  lastFsOrShellGatedCallId(): string | undefined;
49
+ isFsOrShellGatedCall(callId: string): boolean;
49
50
  noteParkGate(evidence: {
50
51
  fsOrShellFamily: boolean;
51
52
  }): void;
@@ -235,6 +235,9 @@ export function createGateLedger() {
235
235
  lastFsOrShellGatedCallId() {
236
236
  return lastFsOrShellGatedCall;
237
237
  },
238
+ isFsOrShellGatedCall(callId) {
239
+ return fsOrShellGatedCalls.has(callId);
240
+ },
238
241
  noteParkGate(evidence) {
239
242
  if (batchParkCount > 0 && !heldSinceLastPark)
240
243
  return;
@@ -56,6 +56,7 @@ export interface HitlDecideDenyOutcome {
56
56
  decision: 'deny';
57
57
  reason?: string | undefined;
58
58
  settledBy?: 'policy' | undefined;
59
+ decidedBy?: 'policy' | undefined;
59
60
  }
60
61
  export declare class HitlSafetyError extends Error {
61
62
  readonly code: HitlSafetyCode;
@@ -2,6 +2,7 @@ import { askParkForeignGateKind } from '../adapter/activeRunSelfHeal.js';
2
2
  import { eventSeq } from '../adapter/types.js';
3
3
  import { hostLog } from '../host.js';
4
4
  import { abortableSleep } from '../abortableSleep.js';
5
+ import { readOwnDataValue } from '../ownKey.js';
5
6
  import { suspendedReopenOf } from './suspendedReopen.js';
6
7
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
7
8
  const MAX_DENY_REASON_CHARS = 4096;
@@ -291,7 +292,11 @@ export class HitlBridge {
291
292
  decision: 'deny',
292
293
  ...this.bindingOf(pending),
293
294
  ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
294
- ...(outcome.settledBy === 'policy' ? { settledBy: 'policy' } : {}),
295
+ ...(readOwnDataValue(outcome, 'decidedBy') === 'policy'
296
+ ? { decidedBy: 'policy' }
297
+ : readOwnDataValue(outcome, 'settledBy') === 'policy'
298
+ ? { settledBy: 'policy' }
299
+ : {}),
295
300
  };
296
301
  return this.decideRaw(pending.sessionId, decision, opts);
297
302
  }
@@ -34,6 +34,7 @@ export declare const EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT: string;
34
34
  export declare function surfaceEditRefusedOnBlindAsk(): void;
35
35
  export declare const RULE_NOT_SENT_WARN_TEXT = "your \"don't ask again\" choice was NOT sent to this engine \u2014 support for that rule form could not be confirmed. The approval itself went through, but you will be asked again";
36
36
  export declare function surfaceRuleArmNotSent(): void;
37
- export declare const RULE_NOT_SENT_REJECTED_WARN_TEXT = "your \"don't ask again\" choice was NOT sent \u2014 it did not pass the client-side check (the card returned a rule the engine never offered, an index that is not a batch offer, or two conflicting choices at once). The approval itself went through, but you will be asked again";
38
- export declare function surfaceRuleArmRejected(): void;
37
+ export type RuleArmRejectedWhy = 'both_arms' | 'index_not_a_batch_offer' | 'text_not_offered';
38
+ export declare const RULE_NOT_SENT_REJECTED_WARN_TEXT: string;
39
+ export declare function surfaceRuleArmRejected(why?: RuleArmRejectedWhy): void;
39
40
  export declare function observeCancelByDeny(settle: Promise<unknown>, taskId: string): void;
@@ -91,11 +91,26 @@ const RULE_NOT_SENT_TIMEOUT_MS = 10_000;
91
91
  export function surfaceRuleArmNotSent() {
92
92
  surfaceSelfClearingWarn(RULE_NOT_SENT_KEY, RULE_NOT_SENT_WARN_TEXT, RULE_NOT_SENT_TIMEOUT_MS);
93
93
  }
94
- export const RULE_NOT_SENT_REJECTED_WARN_TEXT = 'your "don\'t ask again" choice was NOT sent — it did not pass the client-side check (the card returned a rule the engine never offered, an index that is not a batch offer, or two conflicting choices at once). The approval itself went through, but you will be asked again';
94
+ function ruleNotSentRejectedReason(why) {
95
+ switch (why) {
96
+ case 'both_arms':
97
+ return 'the card returned two conflicting choices at once, a rule text and a batch index';
98
+ case 'index_not_a_batch_offer':
99
+ return 'the card returned an index that is not a batch offer';
100
+ case 'text_not_offered':
101
+ return 'the card returned a rule text that is not one of the single rules the engine offered';
102
+ default:
103
+ return 'the card returned a rule text that is not one of the single rules the engine offered, an index that is not a batch offer, or two conflicting choices at once; this client cannot tell which';
104
+ }
105
+ }
106
+ function ruleNotSentRejectedText(why) {
107
+ return `your "don't ask again" choice was NOT sent — it did not pass the client-side check (${ruleNotSentRejectedReason(why)}). The approval itself went through, but you will be asked again`;
108
+ }
109
+ export const RULE_NOT_SENT_REJECTED_WARN_TEXT = ruleNotSentRejectedText();
95
110
  const RULE_NOT_SENT_REJECTED_KEY = 'hitl-rule-arm-rejected';
96
111
  const RULE_NOT_SENT_REJECTED_TIMEOUT_MS = 10_000;
97
- export function surfaceRuleArmRejected() {
98
- surfaceSelfClearingWarn(RULE_NOT_SENT_REJECTED_KEY, RULE_NOT_SENT_REJECTED_WARN_TEXT, RULE_NOT_SENT_REJECTED_TIMEOUT_MS);
112
+ export function surfaceRuleArmRejected(why) {
113
+ surfaceSelfClearingWarn(RULE_NOT_SENT_REJECTED_KEY, ruleNotSentRejectedText(why), RULE_NOT_SENT_REJECTED_TIMEOUT_MS);
99
114
  }
100
115
  function isNoPendingError(e) {
101
116
  if (e instanceof HitlSafetyError)
@@ -1,6 +1,6 @@
1
- import { putOwnKey } from '../ownKey.js';
1
+ import { putOwnKey, readOwnDataValue } from '../ownKey.js';
2
2
  import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, NO_QUESTION_OVERLAY_REASON, findPendingForTask, isNoDecisionSurfaceReason, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
3
- import { ASK_GONE, CHECKPOINT_UNSUPPORTED_VERSION, PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode, refusalSaysStillPending } from '../engineErrorCodes.js';
3
+ import { ASK_GONE, CHECKPOINT_UNSUPPORTED_VERSION, DECIDE_WORKFLOW_PARK_FORKED, DECIDE_WORKFLOW_PARKS_UNREADABLE, PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode, refusalSaysStillPending } from '../engineErrorCodes.js';
4
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
5
5
  import { hostLog } from '../host.js';
6
6
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
@@ -63,7 +63,11 @@ function alreadyResolvedFailureVerdict(outcome) {
63
63
  return isAlreadyResolvedGateReason(outcome.reason);
64
64
  }
65
65
  export function isGateStandingErrorCode(code, retriable) {
66
- return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt' || (code === CHECKPOINT_UNSUPPORTED_VERSION && retriable === true);
66
+ return (code === RESUME_POLICY_FOLD_CODE ||
67
+ code === 'corrupt' ||
68
+ (code === CHECKPOINT_UNSUPPORTED_VERSION && retriable === true) ||
69
+ code === DECIDE_WORKFLOW_PARKS_UNREADABLE ||
70
+ code === DECIDE_WORKFLOW_PARK_FORKED);
67
71
  }
68
72
  export function isCodeClassifiedGateFailure(code, retriable) {
69
73
  return isGateStandingErrorCode(code, retriable) || code === PARKED_RESUME_STARTUP_FAILED;
@@ -192,10 +196,18 @@ function isFetchStepNoPending(outcome) {
192
196
  async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
193
197
  const { deps, led, taskId } = ctx;
194
198
  if (park.gate === 'fs') {
199
+ let decidedByReading;
200
+ try {
201
+ decidedByReading = readOwnDataValue(deps.approvalLane, 'approvalDecidedBy');
202
+ }
203
+ catch {
204
+ decidedByReading = undefined;
205
+ }
195
206
  const outcome = await surfaceFsApprovalAndDecide({
196
207
  client: deps.client,
197
208
  ...(onPresented ? { onPresented } : {}),
198
209
  ...(deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : {}),
210
+ ...(decidedByReading !== undefined ? { approvalDecidedBy: decidedByReading } : {}),
199
211
  }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
200
212
  const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
201
213
  if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
@@ -210,6 +222,12 @@ async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
210
222
  }
211
223
  return surfaceGateAndDecide(deps, taskId, argsByCallOf(led), ctx.signal, gatedCallId, onPresented);
212
224
  }
225
+ function decidedIdentityCandidate(led, park) {
226
+ const id = park.gatedCallId;
227
+ if (id !== undefined)
228
+ return id;
229
+ return led.lastFsOrShellGatedCallId();
230
+ }
213
231
  export async function resolvePark(park, ctx) {
214
232
  const { led, taskId } = ctx;
215
233
  let outcome;
@@ -245,7 +263,7 @@ export async function resolvePark(park, ctx) {
245
263
  }
246
264
  else {
247
265
  if (park.gate === 'fs')
248
- candidateGatedCallId = led.lastFsOrShellGatedCallId();
266
+ candidateGatedCallId = decidedIdentityCandidate(led, park);
249
267
  outcome = await surfaceParkGate(park, ctx, park.gatedCallId, witness);
250
268
  }
251
269
  const alreadyDecidedById = outcome.kind === 'failed' &&
@@ -88,6 +88,10 @@ function rowStr(row, key) {
88
88
  function persistedRuleIdentityRowReadable(row) {
89
89
  return rowStr(row, 'rule') !== undefined && rowStr(row, 'scope') !== undefined;
90
90
  }
91
+ function persistedRuleIdentityRowWhy(row) {
92
+ const bad = ['rule', 'scope'].filter((k) => rowStr(row, k) === undefined);
93
+ return `${bad.join(' and ')} ${bad.length === 1 ? 'is' : 'are'} missing or not a non-empty string`;
94
+ }
91
95
  export function persistedRuleBehaviorOf(rule) {
92
96
  const b = rowStr(rule, 'behavior');
93
97
  return b !== undefined && BEHAVIOR_SET.has(b) ? b : undefined;
@@ -148,16 +152,22 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
148
152
  }
149
153
  return { ok: false, failure };
150
154
  }
151
- if (typeof res !== 'object' ||
152
- res === null ||
153
- !Array.isArray(res.rules) ||
154
- typeof res.rev !== 'number' ||
155
- !Number.isFinite(res.rev)) {
155
+ const pageProblems = [];
156
+ if (typeof res !== 'object' || res === null) {
157
+ pageProblems.push('the page is not an object');
158
+ }
159
+ else {
160
+ if (!Array.isArray(res.rules))
161
+ pageProblems.push('rules is missing or not an array');
162
+ if (typeof res.rev !== 'number' || !Number.isFinite(res.rev))
163
+ pageProblems.push('rev is missing or not a finite number');
164
+ }
165
+ if (pageProblems.length > 0) {
156
166
  return {
157
167
  ok: false,
158
168
  failure: {
159
169
  kind: 'error',
160
- message: 'rule list page was malformed (missing/ill-typed rules array or rev on a 2xx) — refusing to certify it as a complete governance list',
170
+ message: `rule list page was malformed (${pageProblems.join('; ')}) on a 2xx — refusing to certify it as a complete governance list`,
161
171
  },
162
172
  };
163
173
  }
@@ -176,7 +186,7 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
176
186
  ok: false,
177
187
  failure: {
178
188
  kind: 'error',
179
- message: 'rule list page carried a malformed rule row (missing/ill-typed rule or scope) — refusing to present a list that hides or garbles live rules',
189
+ message: `rule list page carried a malformed rule row (${persistedRuleIdentityRowWhy(r)}) — refusing to present a list that hides or garbles live rules`,
180
190
  },
181
191
  };
182
192
  }
@@ -348,24 +358,26 @@ export async function writePersistedRule(facade, input, opts) {
348
358
  catch (e) {
349
359
  return writeOutcomeFromFailure(classifyRulesFailure(e));
350
360
  }
351
- const malformed = () => ({
361
+ const malformed = (what) => ({
352
362
  status: 'unknown',
353
363
  why: 'malformed_result',
354
- message: 'the rule write answered 2xx with a body this client cannot read (status, rev, liveness or the written row) — refusing to certify it as a standing rule; re-list the rules to reconcile',
364
+ message: `the rule write answered 2xx with a body this client cannot read (${what}) — refusing to certify it as a standing rule; re-list the rules to reconcile`,
355
365
  });
356
366
  if (res === null || typeof res !== 'object' || Array.isArray(res))
357
- return malformed();
367
+ return malformed('the body is not an object');
358
368
  const snap = certificationSnapshot(res);
359
369
  if (snap === null)
360
- return malformed();
370
+ return malformed('a field of the body is an inherited or accessor property, or threw when read');
361
371
  const { status, rev, hasLiveness, liveness, hasRow, row } = snap;
362
- if ((status !== 'persisted' && status !== 'no-op') || typeof rev !== 'number' || !Number.isFinite(rev))
363
- return malformed();
372
+ const statusOk = status === 'persisted' || status === 'no-op';
373
+ const revOk = typeof rev === 'number' && Number.isFinite(rev);
374
+ if (!statusOk || !revOk)
375
+ return malformed([...(statusOk ? [] : ['status is not persisted or no-op']), ...(revOk ? [] : ['rev is not a finite number'])].join('; '));
364
376
  const wrote = status === 'persisted' ? 'persisted' : 'no_op';
365
377
  if (hasLiveness) {
366
378
  if (liveness === 'no') {
367
379
  if (hasRow)
368
- return malformed();
380
+ return malformed('liveness and the written row contradict each other');
369
381
  return {
370
382
  status: 'revoked',
371
383
  wrote,
@@ -375,7 +387,7 @@ export async function writePersistedRule(facade, input, opts) {
375
387
  }
376
388
  if (liveness === 'unknown') {
377
389
  if (hasRow)
378
- return malformed();
390
+ return malformed('liveness and the written row contradict each other');
379
391
  return {
380
392
  status: 'unverified',
381
393
  wrote,
@@ -384,10 +396,10 @@ export async function writePersistedRule(facade, input, opts) {
384
396
  };
385
397
  }
386
398
  if (liveness !== 'yes')
387
- return malformed();
399
+ return malformed('liveness is not one of yes, no or unknown');
388
400
  }
389
401
  if (!persistedRuleIdentityRowReadable(row))
390
- return malformed();
402
+ return malformed(`the written row is unreadable (${persistedRuleIdentityRowWhy(row)})`);
391
403
  const landed = row;
392
404
  if (landed.behavior !== behavior || landed.rule !== rule || landed.scope !== scope) {
393
405
  return {
@@ -6,8 +6,6 @@ export declare const PLAN_REVIEW_APPROVE_LABEL = "Yes, approve and run the plan"
6
6
  export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
7
7
  export declare const PLAN_REVIEW_APPROVE_AUTO_LABEL = "Yes, and auto-accept edits";
8
8
  export declare const PLAN_REVIEW_APPROVE_MANUAL_LABEL = "Yes, and manually approve edits";
9
- export declare const PLAN_REVIEW_MODE_AFTER_MIN_ENGINE: readonly [number, number, number];
10
- export declare function versionSupportsPlanReviewModeAfter(v: string | undefined): 'supported' | 'unsupported' | 'unknown';
11
9
  export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined, shownLabels?: readonly string[]): 'approve' | 'reject' | 'dismissed';
12
10
  export type PlanReviewChoice = {
13
11
  decision: 'approve';
@@ -9,7 +9,6 @@ import { decideFailureNote } from '../decideFailureNote.js';
9
9
  import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError } from '../resumeRefusalCopy.js';
10
10
  import { REQUEST_FIELD_CONFLICT, isUnavailableFaceCode } from '../engineErrorCodes.js';
11
11
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
12
- import { engineCapString } from '../engineCapsCache.js';
13
12
  import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
14
13
  import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
15
14
  import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
@@ -20,20 +19,6 @@ export const PLAN_REVIEW_APPROVE_LABEL = 'Yes, approve and run the plan';
20
19
  export const PLAN_REVIEW_REJECT_LABEL = 'No, reject it (keep planning)';
21
20
  export const PLAN_REVIEW_APPROVE_AUTO_LABEL = 'Yes, and auto-accept edits';
22
21
  export const PLAN_REVIEW_APPROVE_MANUAL_LABEL = 'Yes, and manually approve edits';
23
- export const PLAN_REVIEW_MODE_AFTER_MIN_ENGINE = Object.freeze([7, 86, 0]);
24
- export function versionSupportsPlanReviewModeAfter(v) {
25
- const mm = v ? /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(v) : null;
26
- if (!mm)
27
- return 'unknown';
28
- const got = [Number(mm[1]), Number(mm[2]), Number(mm[3])];
29
- for (let i = 0; i < 3; i++) {
30
- const g = got[i];
31
- const need = PLAN_REVIEW_MODE_AFTER_MIN_ENGINE[i];
32
- if (g !== need)
33
- return g > need ? 'supported' : 'unsupported';
34
- }
35
- return 'supported';
36
- }
37
22
  const APPROVE_LABEL = PLAN_REVIEW_APPROVE_LABEL;
38
23
  const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
39
24
  const LEGACY_TWO_CHOICE_LABELS = Object.freeze([PLAN_REVIEW_APPROVE_LABEL, PLAN_REVIEW_REJECT_LABEL]);
@@ -266,9 +251,7 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
266
251
  }
267
252
  armedPlanReviewTaskIds.add(taskId);
268
253
  armedTaskId = taskId;
269
- const capsBaseUrl = wire !== undefined ? wire.capsBaseUrl : engineWireTarget()?.baseUrl;
270
- const offerModeAfter = opts?.submittedInPlanMode === true &&
271
- versionSupportsPlanReviewModeAfter(engineCapString(capsBaseUrl, 'version')) === 'supported';
254
+ const offerModeAfter = opts?.submittedInPlanMode === true;
272
255
  if (offerModeAfter)
273
256
  offerModeAfterByTask.set(taskId, true);
274
257
  else
@@ -109,10 +109,10 @@ export function sessionPolicyDeliverable(behavior, rules, opts) {
109
109
  }
110
110
  if (why === undefined && COVERING_NAMESPACE_PREFIXES.some((p) => r.startsWith(p))) {
111
111
  const segment = protocolSegmentOf(r);
112
- if (segment.kind === 'unknown')
113
- return unreadable();
114
- if (segment.kind === 'unminted')
112
+ if (segment.kind === 'empty' || segment.kind === 'unminted')
115
113
  why = 'not_a_tool_name';
114
+ else if (segment.kind === 'unknown')
115
+ return unreadable();
116
116
  }
117
117
  if (why === undefined && mounted !== undefined && !mounted.has(r))
118
118
  why = 'not_in_roster';
@@ -115,6 +115,8 @@ export declare function sessionPolicyReadNotice(outcome: Exclude<SessionPolicyRe
115
115
  kind: 'present';
116
116
  }> | {
117
117
  kind: 'no_target';
118
+ } | {
119
+ kind: 'not_started';
118
120
  }): string;
119
121
  export interface SessionPolicyRemovedEntry {
120
122
  readonly field: 'toolDeny' | 'toolAllow';
@@ -328,7 +328,7 @@ export function sessionPolicyTightenNotice(outcome) {
328
328
  case 'face_unavailable':
329
329
  return 'This deployment does not keep per-session tool rules, so nothing was written.';
330
330
  case 'session_not_found':
331
- return 'This session is not readable from here — it may not exist, or it may belong to someone else; nothing was written.';
331
+ return 'This session is not readable from here — it may not exist, or it may belong to someone else (the engine answers both the same way, so this client cannot tell which); nothing was written.';
332
332
  case 'session_ownerless':
333
333
  return 'This session has no owner, so a rule written for it could never be read back; nothing was written. Attach an identity to the session and try again.';
334
334
  case 'unauthorized':
@@ -354,6 +354,7 @@ export function sessionPolicyTightenNotice(outcome) {
354
354
  }
355
355
  const READ_NOTICE_GENERIC = 'This session’s rule record could not be shown, so what it holds is not known. Try reading it again.';
356
356
  const READ_NOTICE_NO_TARGET = 'There is no engine connection for this session, so its rule record cannot be read and what it holds is not known. Look again once it is connected.';
357
+ const READ_NOTICE_NOT_STARTED = 'This session has not begun on the engine yet — nothing has been sent in it — so it has no rule record to read. Send a first message, then look again.';
357
358
  function readFailureSentence(kind) {
358
359
  switch (kind) {
359
360
  case 'face-unavailable':
@@ -367,7 +368,7 @@ function readFailureSentence(kind) {
367
368
  case 'session-ownerless':
368
369
  return 'This session has no owner, so it has no rule record to read.';
369
370
  case 'session-not-found':
370
- return 'This session is not readable from here — it may not exist, or it may belong to someone else — so its rule record cannot be shown. Check that you are connected to the engine and the session you meant, or continue in a new session.';
371
+ return 'This session is not readable from here — it may not exist, or it may belong to someone else (the engine answers both the same way, so this client cannot tell which) — so its rule record cannot be shown. Check that you are connected to the engine and the session you meant, or continue in a new session.';
371
372
  case 'request-rejected':
372
373
  return 'The engine rejected the request to read this session’s rule record as malformed, so it could not be read. Sending the same request again will not change that; check which session was asked for.';
373
374
  case 'legacy-tool-name':
@@ -409,6 +410,8 @@ export function sessionPolicyReadNotice(outcome) {
409
410
  return readMalformedSentence(ownRead(outcome, 'why')) ?? READ_NOTICE_GENERIC;
410
411
  if (arm === 'no_target')
411
412
  return READ_NOTICE_NO_TARGET;
413
+ if (arm === 'not_started')
414
+ return READ_NOTICE_NOT_STARTED;
412
415
  return READ_NOTICE_GENERIC;
413
416
  }
414
417
  catch {
@@ -569,7 +572,7 @@ export function sessionPolicyRemovalNotice(outcome) {
569
572
  case 'face_unavailable':
570
573
  return 'This deployment does not keep per-session tool rules, so there is no session rule record to remove entries from — nothing was changed.';
571
574
  case 'session_not_found':
572
- return 'This session is not readable from here — it may not exist, or it may belong to someone else; nothing was changed.';
575
+ return 'This session is not readable from here — it may not exist, or it may belong to someone else (the engine answers both the same way, so this client cannot tell which); nothing was changed.';
573
576
  case 'session_ownerless':
574
577
  return 'This session has no owner, so it has no session rule record to change; nothing was changed.';
575
578
  case 'unauthorized':
@@ -586,7 +589,7 @@ export function sessionPolicyRemovalNotice(outcome) {
586
589
  case 'store_corrupt':
587
590
  return 'The engine could not establish which version this session’s rules are at and refused to stamp a new one, so nothing was changed. This needs an operator to look at the rule store.';
588
591
  case 'session_not_found':
589
- return 'This session is not writable from here — it may not exist, or it may belong to someone else; nothing was changed.';
592
+ return 'This session is not writable from here — it may not exist, or it may belong to someone else (the engine answers both the same way, so this client cannot tell which); nothing was changed.';
590
593
  case 'unauthorized':
591
594
  return 'This call could not present a verifiable identity, so nothing was changed.';
592
595
  default: