@sema-agent/client-core 0.104.0 → 0.105.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 (92) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +13 -5
  3. package/dist/adapt/arms.js +14 -6
  4. package/dist/adapter/activeRunSelfHeal.d.ts +1 -0
  5. package/dist/adapter/activeRunSelfHeal.js +10 -5
  6. package/dist/adapter/downstream/eventToSdkMessage.js +4 -4
  7. package/dist/adapter/downstream/wiringManifestView.d.ts +6 -0
  8. package/dist/adapter/downstream/wiringManifestView.js +25 -0
  9. package/dist/adapter/runStream.js +5 -1
  10. package/dist/cloudConfigWireCaps.js +3 -0
  11. package/dist/compensations.js +6 -6
  12. package/dist/coreValuePorts.d.ts +3 -1
  13. package/dist/coreValuePorts.js +4 -0
  14. package/dist/displayUntrusted.d.ts +4 -2
  15. package/dist/displayUntrusted.js +45 -22
  16. package/dist/engineGenerationPort.d.ts +11 -2
  17. package/dist/engineGenerationPort.js +63 -9
  18. package/dist/engineHealthFacts.d.ts +5 -0
  19. package/dist/engineHealthFacts.js +11 -0
  20. package/dist/eventLeg.d.ts +1 -1
  21. package/dist/eventLeg.js +23 -3
  22. package/dist/generated/engineFactTables.d.ts +1 -0
  23. package/dist/generated/engineFactTables.js +1 -0
  24. package/dist/generated/sdkFactTables.d.ts +2 -0
  25. package/dist/generated/sdkFactTables.js +9 -0
  26. package/dist/headlessReconnectWire.js +31 -4
  27. package/dist/hitl/askGateWire.js +26 -2
  28. package/dist/hitl/frameRouter.d.ts +4 -0
  29. package/dist/hitl/frameRouter.js +14 -0
  30. package/dist/hitl/gateIdentity.d.ts +1 -0
  31. package/dist/hitl/gateIdentity.js +3 -0
  32. package/dist/hitl/livePendingAsk.d.ts +2 -1
  33. package/dist/hitl/livePendingAsk.js +7 -1
  34. package/dist/hitl/planReviewCard.d.ts +21 -0
  35. package/dist/hitl/planReviewCard.js +229 -0
  36. package/dist/hitl/planReviewWire.d.ts +38 -13
  37. package/dist/hitl/planReviewWire.js +355 -133
  38. package/dist/hitl/toolApprovalWire.d.ts +2 -2
  39. package/dist/hitl/toolApprovalWire.js +53 -32
  40. package/dist/hostSettingsSource.d.ts +1 -1
  41. package/dist/hostSettingsSource.js +8 -4
  42. package/dist/hostTurnSettings.d.ts +1 -1
  43. package/dist/hostTurnSettings.js +29 -10
  44. package/dist/i18n/messages.en.d.ts +50 -0
  45. package/dist/i18n/messages.en.js +50 -0
  46. package/dist/i18n/messages.zh.js +51 -0
  47. package/dist/index.d.ts +15 -3
  48. package/dist/index.js +9 -1
  49. package/dist/mcpTransportKindsCapability.d.ts +18 -0
  50. package/dist/mcpTransportKindsCapability.js +75 -0
  51. package/dist/mcpWireCaps.d.ts +4 -1
  52. package/dist/mcpWireCaps.js +24 -13
  53. package/dist/notifications.d.ts +9 -0
  54. package/dist/notifications.js +11 -2
  55. package/dist/ownKey.d.ts +4 -0
  56. package/dist/ownKey.js +8 -0
  57. package/dist/permissionModeWriteCapability.d.ts +13 -0
  58. package/dist/permissionModeWriteCapability.js +35 -0
  59. package/dist/permissionsAllowGrantCapability.d.ts +3 -0
  60. package/dist/permissionsAllowGrantCapability.js +26 -3
  61. package/dist/permissionsDeclaration.d.ts +23 -1
  62. package/dist/permissionsDeclaration.js +60 -30
  63. package/dist/planReviewCardFacts.d.ts +116 -0
  64. package/dist/planReviewCardFacts.js +469 -0
  65. package/dist/reattachWindow.d.ts +8 -1
  66. package/dist/reattachWindow.js +44 -12
  67. package/dist/runFollow.d.ts +30 -0
  68. package/dist/runFollow.js +117 -0
  69. package/dist/runLedgerSnapshot.d.ts +20 -0
  70. package/dist/runLedgerSnapshot.js +272 -0
  71. package/dist/runPermissionModeSwitch.d.ts +57 -0
  72. package/dist/runPermissionModeSwitch.js +135 -0
  73. package/dist/runReattachPolicy.d.ts +13 -5
  74. package/dist/runReattachPolicy.js +58 -23
  75. package/dist/seam.d.ts +3 -0
  76. package/dist/seam.js +1 -1
  77. package/dist/seatContract.js +1 -0
  78. package/dist/sessionMemoryErase.d.ts +1 -1
  79. package/dist/sessionMemoryErase.js +5 -0
  80. package/dist/sessionMemoryStatus.d.ts +27 -1
  81. package/dist/sessionMemoryStatus.js +117 -0
  82. package/dist/sseIdleTriage.d.ts +3 -1
  83. package/dist/sseIdleTriage.js +19 -4
  84. package/dist/steering.d.ts +1 -0
  85. package/dist/steering.js +34 -26
  86. package/dist/subagent/engineSubagentTail.js +2 -0
  87. package/dist/subagentContentStore.d.ts +3 -0
  88. package/dist/subagentContentStore.js +10 -1
  89. package/dist/turnFailurePresentation.d.ts +5 -2
  90. package/dist/turnFailurePresentation.js +12 -4
  91. package/docs/INTEGRATION-CLIENTS.md +587 -10
  92. package/package.json +2 -2
@@ -3,7 +3,17 @@ import { hostEnv } from './hostEnv.js';
3
3
  import { isSdkRunFrameSynthesizable } from './terminalStatuses.js';
4
4
  import { createStreamDrainingLegs } from './streamDrainingLegs.js';
5
5
  import { abortableSleep } from './abortableSleep.js';
6
+ import { mintRunNotFollowed, registerRunNotFollowed } from './runFollow.js';
7
+ import { classifyTurnWireError } from './wireErrorTriage.js';
6
8
  export const HEADLESS_RECONNECT_MAX = 2;
9
+ function guardedTransport(e) {
10
+ try {
11
+ return classifyTurnWireError(e).kind === 'transport';
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
7
17
  function debugLog(msg) {
8
18
  if (hostEnv().SEMA_DEBUG) {
9
19
  console.error(`[sema] headless reconnect: ${msg}`);
@@ -46,6 +56,12 @@ export async function* withHeadlessR1Reconnect(live, deps) {
46
56
  let isRecoverableTear;
47
57
  const drains = createStreamDrainingLegs();
48
58
  let drainReopenAfterMs;
59
+ let lastBreak;
60
+ const giveUp = (fact) => {
61
+ if (typeof originalTearErr === 'object' && originalTearErr !== null)
62
+ registerRunNotFollowed(originalTearErr, fact);
63
+ throw originalTearErr;
64
+ };
49
65
  const terminalFromRunRow = async (taskId) => {
50
66
  const row = await deps.getRun(taskId).catch(() => null);
51
67
  const terminal = terminalEventFromRunRow(row);
@@ -60,9 +76,18 @@ export async function* withHeadlessR1Reconnect(live, deps) {
60
76
  isRecoverableTear = isSseIdleError(originalTearErr);
61
77
  if (!isRecoverableTear)
62
78
  throw originalTearErr;
63
- const taskId = deps.getTaskId();
64
- if (!taskId)
65
- throw originalTearErr;
79
+ let handle;
80
+ try {
81
+ handle = deps.getTaskId();
82
+ }
83
+ catch {
84
+ return giveUp(mintRunNotFollowed('bad_input'));
85
+ }
86
+ if (handle === null || handle === undefined)
87
+ return giveUp(mintRunNotFollowed('no_run_id'));
88
+ if (typeof handle !== 'string' || handle === '')
89
+ return giveUp(mintRunNotFollowed('bad_input'));
90
+ const taskId = handle;
66
91
  if (drainReopenAfterMs !== undefined) {
67
92
  const waitMs = drainReopenAfterMs;
68
93
  drainReopenAfterMs = undefined;
@@ -74,7 +99,7 @@ export async function* withHeadlessR1Reconnect(live, deps) {
74
99
  else {
75
100
  if (attempts >= HEADLESS_RECONNECT_MAX) {
76
101
  debugLog(`budget exhausted (${attempts}/${HEADLESS_RECONNECT_MAX}) — falling to the honest death path`);
77
- throw originalTearErr;
102
+ return giveUp(mintRunNotFollowed('reattach_exhausted', taskId, lastBreak));
78
103
  }
79
104
  attempts++;
80
105
  if (!(await probe(deps.baseUrl)))
@@ -111,6 +136,7 @@ export async function* withHeadlessR1Reconnect(live, deps) {
111
136
  if (deps.signal?.aborted)
112
137
  return;
113
138
  const drained = drains.legEnded('returned');
139
+ lastBreak = drained !== undefined ? 'stream_draining' : undefined;
114
140
  if (drained !== undefined) {
115
141
  if ('reconnectAfterMs' in drained) {
116
142
  drainReopenAfterMs = drained.reconnectAfterMs;
@@ -128,6 +154,7 @@ export async function* withHeadlessR1Reconnect(live, deps) {
128
154
  }
129
155
  catch (contErr) {
130
156
  drains.legEnded('threw');
157
+ lastBreak = guardedTransport(contErr) ? 'connection_lost' : undefined;
131
158
  debugLog(`runs.events continuation failed: ${contErr instanceof Error ? contErr.message : String(contErr)}`);
132
159
  if (deps.signal?.aborted)
133
160
  throw originalTearErr;
@@ -1,9 +1,10 @@
1
1
  import { createGateLedger } from './gateLedger.js';
2
- import { flushHeldWithInterruptRewrite, isHostProgressFrame, routeFrame, } from './frameRouter.js';
2
+ import { bridgeRunHandleFallbackOf, flushHeldWithInterruptRewrite, isHostProgressFrame, routeFrame, } from './frameRouter.js';
3
3
  import { resolvePark, nextHopBudget, stalledTerminal, MAX_GATE_HOPS } from './parkResolver.js';
4
4
  import { createStreamDrainingLegs } from '../streamDrainingLegs.js';
5
5
  import { abortableSleep } from '../abortableSleep.js';
6
6
  import { hostLog } from '../host.js';
7
+ import { invokeHostCallbackIsolated } from '../hostCallbackGuard.js';
7
8
  import { decideAcceptedNotResolved, readDecideReceipt } from '../decideReceipt.js';
8
9
  import { APPROVAL_EXPIRED } from '../engineErrorCodes.js';
9
10
  import { isNotFoundFailure } from '../wireErrorTriage.js';
@@ -37,11 +38,14 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
37
38
  let lastRoundDecided = false;
38
39
  let unverifiableParks = 0;
39
40
  const drains = createStreamDrainingLegs();
41
+ let lastDrainFrame;
40
42
  while (true) {
41
43
  let park = null;
42
44
  for await (const ev of stream) {
43
- if (drains.see(ev))
45
+ if (drains.see(ev)) {
46
+ lastDrainFrame = ev;
44
47
  continue;
48
+ }
45
49
  const answering = led.answeringLiveCards();
46
50
  if (answering.length > 0)
47
51
  await Promise.all(answering);
@@ -77,6 +81,11 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
77
81
  const drained = drains.legEnded('returned');
78
82
  if (drained !== undefined) {
79
83
  const reopenAfterMs = 'reconnectAfterMs' in drained ? drained.reconnectAfterMs : undefined;
84
+ if (taskId.current === undefined) {
85
+ const bound = bridgeRunHandleFallbackOf(deps);
86
+ if (bound !== undefined && reopenAfterMs !== undefined)
87
+ taskId.current = bound;
88
+ }
80
89
  if (reopenAfterMs !== undefined && taskId.current !== undefined && !opts?.signal?.aborted) {
81
90
  hostLog('debug', `liveHitlAskWire: runs.events(${taskId.current}) ended on the drain end frame — the engine is going down; reopening in ${reopenAfterMs}ms`);
82
91
  await abortableSleep(reopenAfterMs, opts?.signal ?? new AbortController().signal);
@@ -91,6 +100,13 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
91
100
  }
92
101
  }
93
102
  hostLog('debug', `liveHitlAskWire: runs.events ended on the drain end frame and is not reopened (${'giveUp' in drained ? drained.giveUp : taskId.current === undefined ? 'no run handle' : 'aborted'}) — ending this leg like a dropped stream`);
103
+ if (!opts?.signal?.aborted) {
104
+ const runId = taskId.current;
105
+ const event = 'giveUp' in drained
106
+ ? { cause: 'stream_draining', ladder: 'exhausted', ...(runId !== undefined ? { runId } : {}) }
107
+ : { cause: 'stream_draining', frame: lastDrainFrame ?? {}, ...(runId !== undefined ? { runId } : {}) };
108
+ notifyBreak(deps, Object.freeze(event), opts?.signal);
109
+ }
94
110
  }
95
111
  yield* flushHeldWithInterruptRewrite(led, { signal: opts?.signal });
96
112
  return;
@@ -161,6 +177,14 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
161
177
  });
162
178
  }
163
179
  }
180
+ function notifyBreak(deps, event, signal) {
181
+ invokeHostCallbackIsolated('liveHitlAskWire: onBreak', () => {
182
+ const cb = deps.onBreak;
183
+ if (typeof cb !== 'function' || signal?.aborted === true)
184
+ return;
185
+ return cb.call(deps, event);
186
+ });
187
+ }
164
188
  export const LATE_DECIDE_OUTCOMES = Object.freeze(['accepted-not-resolved', 'resumed', 'expired-reaped', 'settled-elsewhere', 'unknown']);
165
189
  const LATE_DECIDE_TASK_ID_MAX = 64;
166
190
  export function lateDecideOutcomeOf(reply, opts) {
@@ -5,6 +5,7 @@ import type { ApprovalResolution } from './approvalResolution.js';
5
5
  import type { GateLedger } from './gateLedger.js';
6
6
  import type { NoAllowCause } from './noAllowCause.js';
7
7
  import { type ReplayedApprovalDisposition } from './replayedApproval.js';
8
+ import type { ReattachDrainingEvent } from '../runReattachPolicy.js';
8
9
  export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
9
10
  export declare const HITL_POLICY_DENY_MESSAGE: string;
10
11
  export type PolicyDenyLane = 'no_approval_ui' | 'dont_ask';
@@ -31,11 +32,14 @@ export interface AskGateWireDeps {
31
32
  }) => AsyncGenerator<AgentEvent>;
32
33
  respondToolApproval?: RespondToolApprovalFn;
33
34
  onParkReattach?: (e: ParkReattachNotice) => void;
35
+ onBreak?: (event: ReattachDrainingEvent) => void;
34
36
  approvalLane?: ToolApprovalFrameLaneOpts;
35
37
  onToolApprovalOutcome?: (frame: ToolApprovalFrame, resolution: ApprovalResolution) => void | Promise<void>;
36
38
  onDecideOperation?: DecideOperationObserver;
37
39
  onReplayedApproval?: (frame: ToolApprovalFrame, disposition: ReplayedApprovalDisposition) => void | Promise<void>;
38
40
  }
41
+ export declare function setBridgeRunHandleFallback(deps: object, read: () => string | undefined): void;
42
+ export declare function bridgeRunHandleFallbackOf(deps: object): string | undefined;
39
43
  type GateKind = 'ask' | 'fs';
40
44
  export interface GatePark {
41
45
  gate: GateKind;
@@ -83,6 +83,20 @@ function denyStampedEnd(ev, attribution) {
83
83
  export const HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE = '[Request interrupted by user for tool use]';
84
84
  const RUN_CANCELLED_ERROR_CODE = RUN_CANCELLED_CODE;
85
85
  export const ENGINE_ABORT_TOOL_RESULT = 'Operation aborted';
86
+ const bridgeRunHandleFallbacks = new WeakMap();
87
+ export function setBridgeRunHandleFallback(deps, read) {
88
+ bridgeRunHandleFallbacks.set(deps, read);
89
+ }
90
+ export function bridgeRunHandleFallbackOf(deps) {
91
+ try {
92
+ const read = bridgeRunHandleFallbacks.get(deps);
93
+ const v = read?.();
94
+ return typeof v === 'string' && v !== '' ? v : undefined;
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
86
100
  function isAskToolLoose(name) {
87
101
  return typeof name === 'string' && name.replace(/[\s_-]+/g, '').toLowerCase() === 'askuserquestion';
88
102
  }
@@ -3,6 +3,7 @@ export declare const REOPEN_ID_TAIL = "#reopen-";
3
3
  export declare function approvalCallKey(gatedCallId: string | undefined, taskId: string): string;
4
4
  export declare function askGateQuestionId(callKey: string): string;
5
5
  export declare function planReviewQuestionId(taskId: string): string;
6
+ export declare function freshPlanReviewQuestionId(taskId: string, suffix: string): string;
6
7
  export declare function liveFrameCallKey(approvalId: string): string;
7
8
  export declare function taskIdFromPlanReviewQuestionId(questionId: string): string | undefined;
8
9
  export declare function armedKeyFromQuestionId(questionId: string): string;
@@ -11,6 +11,9 @@ export function askGateQuestionId(callKey) {
11
11
  export function planReviewQuestionId(taskId) {
12
12
  return `${PLAN_REVIEW_QUESTION_ID_PREFIX}${taskId}`;
13
13
  }
14
+ export function freshPlanReviewQuestionId(taskId, suffix) {
15
+ return `${planReviewQuestionId(taskId)}${REOPEN_ID_TAIL}${suffix}`;
16
+ }
14
17
  export function liveFrameCallKey(approvalId) {
15
18
  return `${HITL_FRAME_CALL_KEY_PREFIX}${approvalId}`;
16
19
  }
@@ -1,3 +1,4 @@
1
+ import { type ApprovalRequestFrame } from '@sema-agent/sdk';
1
2
  import type { ApprovalResolution } from './approvalResolution.js';
2
3
  import type { ApprovalsFeedEmission } from './approvalsFeed.js';
3
4
  import { type RespondToolApprovalFn, type ToolApprovalFrame, type ToolApprovalFrameLaneOpts } from './toolApprovalWire.js';
@@ -11,7 +12,7 @@ export interface LivePendingAskView {
11
12
  governanceForced?: true;
12
13
  fromSubagent?: true;
13
14
  originTaskId?: string;
14
- frame?: ToolApprovalFrame;
15
+ frame?: ToolApprovalFrame | ApprovalRequestFrame;
15
16
  }
16
17
  export type LivePendingReading = {
17
18
  kind: 'not_reported';
@@ -1,3 +1,4 @@
1
+ import { isApprovalRequestFrameV1 } from '@sema-agent/sdk';
1
2
  import { appendOwnElement } from '../ownKey.js';
2
3
  import { readRequiresRealApproval } from './approvalCardReading.js';
3
4
  import { THROWN_SLOT_UNREADABLE, readThrownSlot } from '../wireFailureShape.js';
@@ -21,6 +22,11 @@ function snapshotMembers(raw) {
21
22
  }
22
23
  return raw.length === length ? out : undefined;
23
24
  }
25
+ function openingFrameOf(f) {
26
+ if (isToolApprovalFrame(f))
27
+ return f.type === 'tool_approval' ? f : undefined;
28
+ return isApprovalRequestFrameV1(f) ? f : undefined;
29
+ }
24
30
  export function readLivePendingRows(body) {
25
31
  if (body === null || typeof body !== 'object' || Array.isArray(body))
26
32
  return { kind: 'malformed' };
@@ -65,7 +71,7 @@ export function readLivePendingRows(body) {
65
71
  ...(o.governanceForced === true ? { governanceForced: true } : {}),
66
72
  ...(o.fromSubagent === true ? { fromSubagent: true } : {}),
67
73
  ...(nonEmptyString(o.originTaskId) ? { originTaskId: o.originTaskId } : {}),
68
- ...(isToolApprovalFrame(o.frame) && o.frame.type === 'tool_approval' && o.frame.approvalId === o.approvalId ? { frame: o.frame } : {}),
74
+ ...((f) => (f !== undefined && f.approvalId === o.approvalId ? { frame: f } : {}))(openingFrameOf(o.frame)),
69
75
  });
70
76
  }
71
77
  if (members.length > 0 && rows.length === 0)
@@ -0,0 +1,21 @@
1
+ import { type PlanReviewCardFacts, type PlanReviewFactsTaken } from '../planReviewCardFacts.js';
2
+ import { type MessageRef } from '../i18n/translate.js';
3
+ import type { MessageKey } from '../i18n/messages.en.js';
4
+ import type { UiLanguage } from '../env/uiLanguage.js';
5
+ export declare function planReviewCardNoteRef(facts: PlanReviewCardFacts): MessageRef | undefined;
6
+ export declare function planReviewUnreadableNoteRef(): MessageRef;
7
+ export declare function planReviewCardNote(facts: PlanReviewCardFacts, lang?: UiLanguage): string | undefined;
8
+ export declare function planReviewAskAgainKey(facts: PlanReviewCardFacts | undefined): MessageKey;
9
+ export interface PlanReviewLedgerSource {
10
+ readonly runRow: (taskId: string, signal: AbortSignal) => Promise<object>;
11
+ readonly frames: (taskId: string, signal: AbortSignal) => AsyncIterable<object>;
12
+ }
13
+ export type PlanReviewFactsTaking = {
14
+ readonly kind: 'now';
15
+ readonly taken: PlanReviewFactsTaken;
16
+ } | {
17
+ readonly kind: 'ledger';
18
+ readonly pending: Promise<PlanReviewFactsTaken | undefined>;
19
+ };
20
+ export declare function _resetPlanReviewFactsFlightsForTest(): void;
21
+ export declare function takePlanReviewFacts(sessionKey: string, taskId: string, checkpointId: string | undefined, source: () => PlanReviewLedgerSource | undefined, signal?: AbortSignal): PlanReviewFactsTaking;
@@ -0,0 +1,229 @@
1
+ import { currentPlanReviewGateOf, isPlanReviewClaimFrame, mintPlanReviewCardFacts, openPlanReviewLedgerFold, planGateRowReadOf, planReviewBodyOf, planReviewCardFactsFrom, planReviewLedgerClaimOf, planReviewRunGeneration, beginPlanReviewFactsRead, commitPlanReviewFacts, samePlanClaim, stepPlanReviewLedgerFold, unknownBody, } from '../planReviewCardFacts.js';
2
+ import { msg, renderMessage } from '../i18n/translate.js';
3
+ import { readRunLedgerSnapshot } from '../runLedgerSnapshot.js';
4
+ import { snapshotIsSubFlow, snapshotSegmentIdentity } from '../adapter/types.js';
5
+ import { settleWithinDeadline } from '../abortableSleep.js';
6
+ import { hostLog } from '../host.js';
7
+ export function planReviewCardNoteRef(facts) {
8
+ const body = facts.body;
9
+ switch (body.kind) {
10
+ case 'shown':
11
+ return body.markdown.trim() === '' ? msg('approval.planReview.note.empty') : undefined;
12
+ case 'marked':
13
+ return msg('approval.planReview.note.marked');
14
+ case 'too_large':
15
+ return msg('approval.planReview.note.tooLarge', { length: body.length });
16
+ case 'unknown':
17
+ return msg(body.cause === 'not_carried' ? 'approval.planReview.note.notCarried' : 'approval.planReview.note.unknown');
18
+ }
19
+ }
20
+ export function planReviewUnreadableNoteRef() {
21
+ return msg('approval.planReview.note.unknown');
22
+ }
23
+ export function planReviewCardNote(facts, lang) {
24
+ let ref;
25
+ try {
26
+ const read = planReviewCardFactsFrom(facts);
27
+ ref = read === undefined ? planReviewUnreadableNoteRef() : planReviewCardNoteRef(read);
28
+ }
29
+ catch {
30
+ ref = planReviewUnreadableNoteRef();
31
+ }
32
+ return ref === undefined ? undefined : renderMessage(ref, lang);
33
+ }
34
+ export function planReviewAskAgainKey(facts) {
35
+ const body = facts?.body;
36
+ if (body === undefined)
37
+ return 'approval.planReview.askAgain.unknown';
38
+ switch (body.kind) {
39
+ case 'shown':
40
+ return 'approval.planReview.askAgain.shown';
41
+ case 'marked':
42
+ return 'approval.planReview.askAgain.marked';
43
+ case 'too_large':
44
+ return 'approval.planReview.askAgain.tooLarge';
45
+ case 'unknown':
46
+ return body.cause === 'not_carried' ? 'approval.planReview.askAgain.notCarried' : 'approval.planReview.askAgain.unknown';
47
+ }
48
+ }
49
+ const PLAN_REVIEW_LEDGER_BUDGET_MS = 3_000;
50
+ const planReviewFactsFlights = new Map();
51
+ export function _resetPlanReviewFactsFlightsForTest() {
52
+ planReviewFactsFlights.clear();
53
+ }
54
+ export function takePlanReviewFacts(sessionKey, taskId, checkpointId, source, signal) {
55
+ const now = (facts, generation) => ({ kind: 'now', taken: { sessionKey, taskId, facts, generation } });
56
+ try {
57
+ const g0 = planReviewRunGeneration(sessionKey, taskId);
58
+ const gate = currentPlanReviewGateOf(sessionKey, taskId);
59
+ if (checkpointId !== undefined && gate !== undefined && gate.checkpointId === checkpointId)
60
+ return now(gate.facts, gate.generation);
61
+ const flightKey = `${sessionKey}\u0000${taskId}\u0000${checkpointId ?? ''}`;
62
+ const inFlight = planReviewFactsFlights.get(flightKey);
63
+ if (inFlight !== undefined)
64
+ return { kind: 'ledger', pending: inFlight };
65
+ let src;
66
+ try {
67
+ src = source();
68
+ }
69
+ catch {
70
+ src = undefined;
71
+ }
72
+ if (src === undefined)
73
+ return now(mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('not_observed')), g0);
74
+ const reader = src;
75
+ const started = Date.now();
76
+ const ledger = (budgetMs) => readPlanReviewFactsFromLedger(beginPlanReviewFactsRead(sessionKey, taskId), checkpointId, reader, signal, budgetMs);
77
+ const viaRow = checkpointId === undefined && gate !== undefined
78
+ ? readPlanGateRowWithin(reader, taskId, signal, PLAN_REVIEW_LEDGER_BUDGET_MS).then((row) => {
79
+ const cur = currentPlanReviewGateOf(sessionKey, taskId);
80
+ if (row.ok && row.atPlanGate && row.checkpointId !== undefined && cur !== undefined && cur.checkpointId === row.checkpointId) {
81
+ return { sessionKey, taskId, facts: cur.facts, generation: cur.generation };
82
+ }
83
+ if (!row.ok)
84
+ return { sessionKey, taskId, facts: mintPlanReviewCardFacts(taskId, undefined, unknownBody('unreadable')), generation: g0 };
85
+ return ledger(Math.max(0, PLAN_REVIEW_LEDGER_BUDGET_MS - (Date.now() - started)));
86
+ })
87
+ : undefined;
88
+ const pending = (viaRow ?? ledger(PLAN_REVIEW_LEDGER_BUDGET_MS)).then((taken) => {
89
+ if (planReviewFactsFlights.get(flightKey) === pending)
90
+ planReviewFactsFlights.delete(flightKey);
91
+ return taken;
92
+ }, () => {
93
+ if (planReviewFactsFlights.get(flightKey) === pending)
94
+ planReviewFactsFlights.delete(flightKey);
95
+ return undefined;
96
+ });
97
+ planReviewFactsFlights.set(flightKey, pending);
98
+ return { kind: 'ledger', pending };
99
+ }
100
+ catch {
101
+ return now(mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable')), Number.NaN);
102
+ }
103
+ }
104
+ const rowUnread = () => ({ ok: false, atPlanGate: false });
105
+ async function readPlanGateRow(source, taskId, signal) {
106
+ try {
107
+ return planGateRowReadOf(await source.runRow(taskId, signal));
108
+ }
109
+ catch {
110
+ return rowUnread();
111
+ }
112
+ }
113
+ async function readPlanGateRowWithin(source, taskId, outer, budgetMs) {
114
+ if (outer?.aborted === true)
115
+ return rowUnread();
116
+ let onOuterAbort;
117
+ try {
118
+ const aborted = new Promise((resolve) => {
119
+ onOuterAbort = () => resolve(rowUnread());
120
+ outer?.addEventListener('abort', onOuterAbort, { once: true });
121
+ });
122
+ const read = settleWithinDeadline(budgetMs, (deadline) => {
123
+ const ac = new AbortController();
124
+ const stop = () => ac.abort();
125
+ deadline.addEventListener('abort', stop, { once: true });
126
+ outer?.addEventListener('abort', stop, { once: true });
127
+ return readPlanGateRow(source, taskId, ac.signal).finally(() => {
128
+ deadline.removeEventListener('abort', stop);
129
+ outer?.removeEventListener('abort', stop);
130
+ });
131
+ }).then((settled) => (settled.kind === 'value' ? settled.value : rowUnread()));
132
+ return await Promise.race([read, aborted]);
133
+ }
134
+ catch {
135
+ return rowUnread();
136
+ }
137
+ finally {
138
+ if (onOuterAbort !== undefined)
139
+ outer?.removeEventListener('abort', onOuterAbort);
140
+ }
141
+ }
142
+ function foldLedgerFrame(fold, ev) {
143
+ if (typeof ev !== 'object' || ev === null)
144
+ return fold;
145
+ const snap = snapshotSegmentIdentity(ev);
146
+ return stepPlanReviewLedgerFold(fold, ev, snap === null || snapshotIsSubFlow(snap));
147
+ }
148
+ async function readPlanReviewFactsFromLedger(ticket, given, source, signal, budgetMs) {
149
+ const { sessionKey, taskId } = ticket;
150
+ const reads = [];
151
+ let claim;
152
+ let highWater;
153
+ try {
154
+ const snap = await readRunLedgerSnapshot(taskId, undefined, {
155
+ isParked: async (id, s) => {
156
+ const r = await readPlanGateRow(source, id, s);
157
+ reads.push(r);
158
+ const first = reads[0];
159
+ if (!r.ok || !r.atPlanGate || r.checkpointId === undefined)
160
+ return false;
161
+ if (given !== undefined && r.checkpointId !== given)
162
+ return false;
163
+ return reads.length === 1 || (first !== undefined && first.ok && first.checkpointId === r.checkpointId);
164
+ },
165
+ frames: (id, s) => source.frames(id, s),
166
+ budgetMs,
167
+ log: (msg) => {
168
+ hostLog('debug', `planReviewCard: ${msg}`);
169
+ },
170
+ fold: {
171
+ init: openPlanReviewLedgerFold,
172
+ step: foldLedgerFrame,
173
+ strictOrder: true,
174
+ substantive: isPlanReviewClaimFrame,
175
+ agree: (a, b) => {
176
+ const x = planReviewLedgerClaimOf(a);
177
+ const y = planReviewLedgerClaimOf(b);
178
+ return x === undefined || y === undefined ? x === y : samePlanClaim(x, y);
179
+ },
180
+ },
181
+ }, signal);
182
+ if (snap !== null && snap.folded !== undefined) {
183
+ claim = planReviewLedgerClaimOf(snap.folded);
184
+ if (claim !== undefined)
185
+ highWater = snap.highWater;
186
+ }
187
+ }
188
+ catch {
189
+ claim = undefined;
190
+ }
191
+ const first = reads[0];
192
+ const firstGate = first !== undefined && first.ok && first.atPlanGate ? first.checkpointId : undefined;
193
+ let facts;
194
+ if (claim !== undefined && firstGate !== undefined) {
195
+ facts = mintPlanReviewCardFacts(taskId, firstGate, claim.kind === 'text' ? planReviewBodyOf(claim.text) : unknownBody(claim.cause));
196
+ }
197
+ else {
198
+ const missed = ledgerMissOf(reads, given);
199
+ claim = { kind: 'cause', cause: missed.cause };
200
+ highWater = undefined;
201
+ facts = mintPlanReviewCardFacts(taskId, missed.checkpointId, unknownBody(missed.cause));
202
+ }
203
+ const out = commitPlanReviewFacts(ticket, facts, claim, highWater);
204
+ if (out === undefined) {
205
+ hostLog('debug', `planReviewCard: ledger-read-superseded task=${taskId}`);
206
+ return undefined;
207
+ }
208
+ return { sessionKey, taskId, facts: out, generation: ticket.generation };
209
+ }
210
+ function ledgerMissOf(reads, given) {
211
+ const first = reads[0];
212
+ if (first === undefined || !first.ok)
213
+ return { cause: 'unreadable', ...(given !== undefined ? { checkpointId: given } : {}) };
214
+ if (!first.atPlanGate || first.checkpointId === undefined)
215
+ return { cause: 'gate_unidentified' };
216
+ const asked = given ?? first.checkpointId;
217
+ if (given !== undefined && first.checkpointId !== given)
218
+ return { cause: 'gate_moved', checkpointId: asked };
219
+ const second = reads[1];
220
+ if (second !== undefined) {
221
+ if (!second.ok)
222
+ return { cause: 'unreadable', checkpointId: asked };
223
+ if (!second.atPlanGate || second.checkpointId === undefined)
224
+ return { cause: 'gate_unidentified' };
225
+ if (second.checkpointId !== first.checkpointId)
226
+ return { cause: 'gate_moved', checkpointId: asked };
227
+ }
228
+ return { cause: 'unreadable', checkpointId: asked };
229
+ }
@@ -2,34 +2,52 @@ import { type QuestionAnswer } from '../liveQuestionStore.js';
2
2
  import type { EngineWireInjection } from '../engineWireSdk.js';
3
3
  import { type PlanReviewModeAfter } from './hitlBridge.js';
4
4
  import type { AgentClient } from '@sema-agent/sdk';
5
+ import { isPlanReviewPark, type PlanReviewCardFacts } from '../planReviewCardFacts.js';
6
+ import type { UiLanguage } from '../env/uiLanguage.js';
5
7
  import type { ReopenCardVerdict } from '../adapter/activeRunSelfHeal.js';
6
8
  export declare const PLAN_REVIEW_APPROVE_LABEL = "Yes, approve and run the plan";
7
9
  export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
8
10
  export declare const PLAN_REVIEW_APPROVE_AUTO_LABEL = "Yes, and auto-accept edits";
9
11
  export declare const PLAN_REVIEW_APPROVE_MANUAL_LABEL = "Yes, and manually approve edits";
10
- export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined, shownLabels?: readonly string[]): 'approve' | 'reject' | 'dismissed';
12
+ export type PlanReviewOptionChoice = 'approve' | 'approve_accept_edits' | 'approve_manual' | 'reject' | 'ask_again';
13
+ export interface PlanReviewOption {
14
+ readonly label: string;
15
+ readonly description: string;
16
+ readonly choice: PlanReviewOptionChoice;
17
+ }
18
+ type PlanReviewNonDecision = 'dismissed' | 'inadmissible';
19
+ export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined, shownLabels?: readonly string[]): 'approve' | 'reject' | 'ask_again' | PlanReviewNonDecision;
11
20
  export type PlanReviewChoice = {
12
21
  decision: 'approve';
13
22
  permissionModeAfter?: PlanReviewModeAfter;
14
23
  } | {
15
24
  decision: 'reject';
16
25
  reason?: string;
26
+ } | {
27
+ decision: 'ask_again';
28
+ reason?: string;
17
29
  };
18
- export declare function planReviewChoiceFromAnswer(answer: QuestionAnswer | null | undefined, shownLabels?: readonly string[]): PlanReviewChoice | 'dismissed';
30
+ export declare function planReviewChoiceFromAnswer(answer: QuestionAnswer | null | undefined, shownLabels?: readonly string[]): PlanReviewChoice | PlanReviewNonDecision;
19
31
  export declare function planReviewCardOptions(offerModeAfter: boolean): Array<{
20
32
  label: string;
21
33
  description: string;
22
34
  }>;
35
+ export declare function planReviewOptionsOf(facts: PlanReviewCardFacts, opts?: {
36
+ readonly submittedInPlanMode?: boolean;
37
+ }): readonly PlanReviewOption[];
38
+ export interface PlanReviewCardView {
39
+ readonly facts?: PlanReviewCardFacts;
40
+ readonly options: readonly PlanReviewOption[];
41
+ readonly note?: string;
42
+ }
43
+ export declare function planReviewCardViewOf(frame: object | null | undefined, lang?: UiLanguage): PlanReviewCardView | undefined;
23
44
  export declare function notePlanReviewAnsweredIfDecisive(questionId: unknown, answer: QuestionAnswer | null | undefined): void;
24
- export declare function isPlanReviewPark(result: unknown): result is {
25
- taskId: string;
26
- sessionId?: string;
27
- };
45
+ export { isPlanReviewPark };
28
46
  export declare function _resetArmedPlanReviewsForTest(): void;
29
47
  export type PlanReviewReopenTrigger = 'user' | 'automatic';
30
48
  export type PlanReviewGateInstanceReader = (taskId: string) => Promise<ReadonlySet<string> | null>;
31
49
  export declare function clearPlanReviewUserDismissals(sessionKey?: string): void;
32
- export type PlanReviewOfferedMode = 'three_choice' | 'two_choice' | 'unknown';
50
+ export type PlanReviewOfferedMode = 'three_choice' | 'two_choice' | 'withheld' | 'unknown';
33
51
  export interface PlanReviewCardLike {
34
52
  readonly questionId?: string;
35
53
  readonly questions?: ReadonlyArray<{
@@ -45,8 +63,8 @@ export interface PlanReviewQuestionIdReading {
45
63
  export declare function planReviewTaskIdFromQuestionId(questionId: {} | null | undefined): PlanReviewQuestionIdReading | undefined;
46
64
  export declare function planReviewOfferedModeOf(card: string | PlanReviewCardLike): PlanReviewOfferedMode;
47
65
  export declare function planReviewShownLabelsOf(questionId: {} | null | undefined): readonly string[] | undefined;
48
- export declare function planReviewAnswerChoiceOf(questionId: {} | null | undefined, answer: QuestionAnswer | null | undefined): PlanReviewChoice | 'dismissed';
49
- export declare function planReviewAnswerDecisionOf(questionId: {} | null | undefined, answer: QuestionAnswer | null | undefined): 'approve' | 'reject' | 'dismissed';
66
+ export declare function planReviewAnswerChoiceOf(questionId: {} | null | undefined, answer: QuestionAnswer | null | undefined): PlanReviewChoice | PlanReviewNonDecision;
67
+ export declare function planReviewAnswerDecisionOf(questionId: {} | null | undefined, answer: QuestionAnswer | null | undefined): 'approve' | 'reject' | 'ask_again' | PlanReviewNonDecision;
50
68
  export interface ArmPlanReviewOptions {
51
69
  submittedInPlanMode?: boolean;
52
70
  readGateInstanceKeys?: PlanReviewGateInstanceReader;
@@ -58,14 +76,14 @@ export type PlanReviewDecisionEffect = 'took_effect' | 'advanced' | 'still_parke
58
76
  export interface PlanReviewOutcomeMeta {
59
77
  taskId: string;
60
78
  dispatchNo: number;
61
- decision: 'approve' | 'reject';
79
+ decision: 'approve' | 'reject' | 'ask_again';
62
80
  effect: PlanReviewDecisionEffect;
63
81
  cause?: 'stored_form_unsupported';
64
82
  }
65
83
  export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect, cause?: 'stored_form_unsupported'): string;
66
84
  export type PlanReviewWireClient = {
67
85
  assistant: Pick<AgentClient['assistant'], 'planReview'>;
68
- runs: Pick<AgentClient['runs'], 'get'>;
86
+ runs: Pick<AgentClient['runs'], 'get'> & Partial<Pick<AgentClient['runs'], 'events'>>;
69
87
  };
70
88
  export interface PlanReviewWire extends EngineWireInjection<PlanReviewWireClient> {
71
89
  client: PlanReviewWireClient;
@@ -95,17 +113,18 @@ export declare function notePlanReviewDecisionNotApplied(taskId: string): void;
95
113
  export declare function notePlanReviewGateLeft(taskId: string): void;
96
114
  export declare function latestPlanReviewDecisionMark(taskId: string): number | null;
97
115
  export declare function __resetPlanReviewDecisionLatchForTests(): void;
98
- export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject', permissionModeAfter?: string, opts?: DecidePlanReviewOptions): Promise<void>;
116
+ export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject' | 'ask_again', permissionModeAfter?: string, opts?: DecidePlanReviewOptions): Promise<void>;
99
117
  export interface ReopenPlanReviewSharedOpts {
100
118
  sessionKey?: string;
101
119
  presentationReceiptMs: number;
102
120
  trigger?: PlanReviewReopenTrigger;
103
121
  readGateInstanceKeys?: PlanReviewGateInstanceReader;
104
122
  pendingReadStartedAt?: number;
123
+ checkpointId?: string;
105
124
  }
106
125
  export interface ReopenPlanReviewFreshOpts extends ReopenPlanReviewSharedOpts {
107
126
  mintFreshQuestionId?: true;
108
- deliverDecision?: (taskId: string, decision: 'approve' | 'reject', reason?: string) => void | Promise<void>;
127
+ deliverDecision?: (taskId: string, decision: Parameters<typeof decidePlanReview>[1], reason?: string) => void | Promise<void>;
109
128
  wire?: PlanReviewWire;
110
129
  onOutcome?: PlanReviewDeliveryOptions['onOutcome'];
111
130
  }
@@ -118,3 +137,9 @@ export interface ReopenPlanReviewReuseOpts extends ReopenPlanReviewSharedOpts {
118
137
  export type ReopenPlanReviewOpts = ReopenPlanReviewFreshOpts | ReopenPlanReviewReuseOpts;
119
138
  export declare function _resetActiveReopenRespondersForTest(): void;
120
139
  export declare function reopenPlanReviewCard(taskId: string, opts: ReopenPlanReviewOpts): Promise<ReopenCardVerdict>;
140
+ export interface ReadPlanReviewCardFactsOptions {
141
+ readonly checkpointId?: string;
142
+ readonly wire?: PlanReviewWire;
143
+ readonly signal?: AbortSignal;
144
+ }
145
+ export declare function readPlanReviewCardFacts(taskId: string, opts?: ReadPlanReviewCardFactsOptions): Promise<PlanReviewCardFacts>;