@sema-agent/client-core 0.106.0 → 0.107.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 (52) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +20 -16
  3. package/dist/adapter/activeRunSelfHeal.d.ts +8 -0
  4. package/dist/adapter/activeRunSelfHeal.js +50 -22
  5. package/dist/checkpointRowVerdict.d.ts +6 -0
  6. package/dist/checkpointRowVerdict.js +9 -0
  7. package/dist/decideReceipt.js +1 -1
  8. package/dist/displayUntrusted.d.ts +5 -3
  9. package/dist/displayUntrusted.js +50 -41
  10. package/dist/engineNoticeCodes.d.ts +12 -0
  11. package/dist/engineNoticeCodes.js +117 -6
  12. package/dist/errorSupplementRoles.js +2 -1
  13. package/dist/executionLaneCapability.js +3 -2
  14. package/dist/executionLaneWords.d.ts +1 -0
  15. package/dist/executionLaneWords.js +3 -0
  16. package/dist/generated/engineNoticeTables.js +9 -3
  17. package/dist/hitl/approvalResolution.d.ts +5 -1
  18. package/dist/hitl/approvalResolution.js +18 -1
  19. package/dist/hitl/approvalsFeed.js +2 -1
  20. package/dist/hitl/askParkRowRouting.js +12 -0
  21. package/dist/hitl/decisionSend.d.ts +2 -0
  22. package/dist/hitl/decisionSend.js +20 -6
  23. package/dist/hitl/hitlBridge.d.ts +5 -1
  24. package/dist/hitl/hitlBridge.js +27 -2
  25. package/dist/hitl/livePendingAsk.js +4 -2
  26. package/dist/hitl/parkOwnership.js +4 -5
  27. package/dist/hitl/parkResolver.js +9 -5
  28. package/dist/hitl/parkRowBirthWait.d.ts +7 -0
  29. package/dist/hitl/parkRowBirthWait.js +1 -1
  30. package/dist/hitl/parkRowDelegation.d.ts +6 -0
  31. package/dist/hitl/parkRowDelegation.js +33 -0
  32. package/dist/hitl/persistedRulesWire.d.ts +15 -3
  33. package/dist/hitl/persistedRulesWire.js +102 -20
  34. package/dist/hitl/planReviewWire.js +4 -2
  35. package/dist/hitl/sessionAllowRoute.d.ts +5 -0
  36. package/dist/hitl/sessionAllowRoute.js +42 -0
  37. package/dist/hitl/standingRows.js +10 -10
  38. package/dist/hitl/toolApprovalWire.js +22 -8
  39. package/dist/i18n/messages.en.d.ts +6 -0
  40. package/dist/i18n/messages.en.js +8 -2
  41. package/dist/i18n/messages.zh.js +9 -2
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1 -0
  44. package/dist/injectionLaneNotice.js +2 -1
  45. package/dist/noticePresentation.js +2 -0
  46. package/dist/sdkWireTransit.d.ts +1 -1
  47. package/dist/seatContract.d.ts +1 -0
  48. package/dist/seatContract.js +3 -1
  49. package/dist/wireRefusalCopy.d.ts +6 -1
  50. package/dist/wireRefusalCopy.js +7 -1
  51. package/docs/INTEGRATION-CLIENTS.md +382 -13
  52. package/package.json +5 -5
@@ -2,6 +2,7 @@ import { isOwnEngineRun } from '../subagentContentStore.js';
2
2
  import { rowIdTail } from '../workflow.js';
3
3
  import { hostSessionFor } from '../host.js';
4
4
  import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
5
+ import { pendingRowIsOfSession } from './parkRowDelegation.js';
5
6
  export function pendingRowIsOwnedByThisSession(row, deps) {
6
7
  const sessionKey = deps?.sessionKey ?? DEFAULT_SESSION_KEY;
7
8
  const rowTaskId = typeof row.taskId === 'string' ? row.taskId : '';
@@ -14,11 +15,9 @@ export function pendingRowIsOwnedByThisSession(row, deps) {
14
15
  }
15
16
  const currentSessionId = deps?.currentSessionId ?? (() => hostSessionFor(sessionKey)?.currentSessionId());
16
17
  const sessionId = currentSessionId();
17
- const rowSessionId = typeof row.sessionId === 'string' ? row.sessionId : '';
18
- return (typeof sessionId === 'string' &&
19
- sessionId.length > 0 &&
20
- rowSessionId.length > 0 &&
21
- rowSessionId === sessionId);
18
+ if (typeof sessionId !== 'string' || sessionId.length === 0)
19
+ return false;
20
+ return pendingRowIsOfSession(row, sessionId);
22
21
  }
23
22
  export function askQuestionsFromPending(pending) {
24
23
  const input = pending.input;
@@ -12,6 +12,7 @@ import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
12
12
  import { readDecideReceipt } from '../decideReceipt.js';
13
13
  import { decideFailureNote } from '../decideFailureNote.js';
14
14
  import { wireFailureTextsOf } from '../wireFailureShape.js';
15
+ import { msg, renderMessage } from '../i18n/translate.js';
15
16
  export const MAX_GATE_HOPS = 2;
16
17
  export function nextHopBudget(prev, round) {
17
18
  const total = (prev?.total ?? 0) + 1;
@@ -63,7 +64,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
63
64
  }
64
65
  const found = await findPendingForTask(deps.client, taskId, isAskTool, signal ? { signal } : undefined, parkGatedCallId, (row) => isAskTool(typeof row.toolName === 'string' ? row.toolName : undefined));
65
66
  if (!found.ok)
66
- return { kind: 'not_sent', lane: 'ask', cause: found.code ?? 'fetch_failed', ...(found.reason !== '' ? { detail: found.reason } : {}) };
67
+ return { kind: 'not_sent', lane: 'ask', cause: found.code ?? 'fetch_failed', ...(found.reason !== '' ? { detail: found.reason } : {}), ...(found.rowVerdict !== undefined ? { rowVerdict: found.rowVerdict } : {}) };
67
68
  const { pending, gatedCallId } = found;
68
69
  const at = { lane: 'ask', ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
69
70
  const fromArgs = gatedCallId
@@ -284,7 +285,8 @@ export async function resolvePark(park, ctx) {
284
285
  ? 'the approval card was retracted by the host without a decision (nothing was sent; the run stays parked and remains decidable elsewhere)'
285
286
  : failureReasonOf(outcome);
286
287
  const noDecisionSurface = outcome.kind === 'not_sent' && outcome.cause === 'card_unavailable' && outcome.detail !== undefined && isNoDecisionSurfaceReason(outcome.detail);
287
- return { kind: 'failsoft', events: failsoftEvents(park, ctx, interrupted, failedReason, undefined, noDecisionSurface) };
288
+ const undecidableHere = outcome.kind === 'not_sent' && (outcome.cause === 'row_unresumable' || (outcome.cause === 'safety_stop' && outcome.safetyCode === 'binding_missing'));
289
+ return { kind: 'failsoft', events: failsoftEvents(park, ctx, interrupted, failedReason, undefined, noDecisionSurface, undecidableHere) };
288
290
  }
289
291
  led.dropHeldForDecidedPark(outcome.gatedCallId, park.gatedCallId);
290
292
  if (outcome.gatedCallId) {
@@ -307,15 +309,17 @@ export async function resolvePark(park, ctx) {
307
309
  ...(handoffTaskId !== undefined ? { handoffTaskId } : {}),
308
310
  };
309
311
  }
310
- function failsoftReasonSentence(park, failedReason, terminal) {
312
+ function failsoftReasonSentence(park, failedReason, terminal, reasonCarriesWayOut = false) {
311
313
  if (terminal !== undefined)
312
314
  return terminal.errorMessage;
313
315
  if (failedReason === undefined)
314
316
  return undefined;
317
+ if (reasonCarriesWayOut)
318
+ return renderMessage(msg('approval.park.stillParked', { gate: park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion', reason: failedReason }));
315
319
  return (`${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
316
320
  ' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.');
317
321
  }
318
- function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, noDecisionSurface = false) {
322
+ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, noDecisionSurface = false, reasonCarriesWayOut = false) {
319
323
  const events = [
320
324
  ...flushHeldWithInterruptRewrite(ctx.led, {
321
325
  terminal: park.pendingDone,
@@ -323,7 +327,7 @@ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, no
323
327
  gateAbortedByUser,
324
328
  }),
325
329
  ];
326
- const reason = failsoftReasonSentence(park, failedReason, terminal);
330
+ const reason = failsoftReasonSentence(park, failedReason, terminal, reasonCarriesWayOut);
327
331
  if (park.pendingDone) {
328
332
  events.push(reason === undefined || noDecisionSurface ? park.pendingDone : inheritFrameBinding(park.pendingDone, { ...park.pendingDone, _sema_park_failed_reason: reason }));
329
333
  }
@@ -1,3 +1,4 @@
1
+ import type { CheckpointRowVerdictReading } from '../checkpointRowVerdict.js';
1
2
  export type ParkRowProbe<Row> = {
2
3
  kind: 'row';
3
4
  row: Row;
@@ -7,6 +8,9 @@ export type ParkRowProbe<Row> = {
7
8
  } | {
8
9
  kind: 'settled';
9
10
  reason: string;
11
+ rowVerdict?: Exclude<CheckpointRowVerdictReading, {
12
+ readonly kind: 'ok';
13
+ }>;
10
14
  };
11
15
  export type ParkRowWaitVerdict<Row> = {
12
16
  kind: 'row';
@@ -23,6 +27,9 @@ export type ParkRowWaitVerdict<Row> = {
23
27
  reason: string;
24
28
  waitedMs: number;
25
29
  probes: number;
30
+ rowVerdict?: Exclude<CheckpointRowVerdictReading, {
31
+ readonly kind: 'ok';
32
+ }>;
26
33
  } | {
27
34
  kind: 'aborted';
28
35
  waitedMs: number;
@@ -103,7 +103,7 @@ export async function waitForParkRowBirth(deps) {
103
103
  if (probed.kind === 'row')
104
104
  return { kind: 'row', row: probed.row, waitedMs: now() - startedAt, probes };
105
105
  if (probed.kind === 'settled') {
106
- return { kind: 'settled', reason: probed.reason, waitedMs: now() - startedAt, probes };
106
+ return { kind: 'settled', reason: probed.reason, waitedMs: now() - startedAt, probes, ...(probed.rowVerdict !== undefined ? { rowVerdict: probed.rowVerdict } : {}) };
107
107
  }
108
108
  lastReason = probed.reason;
109
109
  const remainingMs = budgetMs - (now() - startedAt);
@@ -0,0 +1,6 @@
1
+ export interface ParkRowDelegationLinks {
2
+ readonly parentSessionId?: string;
3
+ readonly rootSessionId?: string;
4
+ }
5
+ export declare function pendingRowIsOfSession(row: object, sessionId: string): boolean;
6
+ export declare function parkRowDelegationLinksOf(row: object | null | undefined): ParkRowDelegationLinks | undefined;
@@ -0,0 +1,33 @@
1
+ import { slotReadOf } from '../wireFailureShape.js';
2
+ export function pendingRowIsOfSession(row, sessionId) {
3
+ if (sessionId.length === 0)
4
+ return false;
5
+ const rowSessionId = typeof row.sessionId === 'string' ? row.sessionId : '';
6
+ if (rowSessionId.length > 0 && rowSessionId === sessionId)
7
+ return true;
8
+ const links = parkRowDelegationLinksOf(row);
9
+ return links !== undefined && (links.parentSessionId === sessionId || links.rootSessionId === sessionId);
10
+ }
11
+ export function parkRowDelegationLinksOf(row) {
12
+ try {
13
+ if (typeof row !== 'object' || row === null)
14
+ return undefined;
15
+ const a = slotReadOf(row, 'agent');
16
+ if (a.presence !== 'present' || typeof a.value !== 'object' || a.value === null)
17
+ return undefined;
18
+ const agent = a.value;
19
+ const link = (key) => {
20
+ const r = slotReadOf(agent, key);
21
+ return r.presence === 'present' && typeof r.value === 'string' && r.value !== '' ? r.value : undefined;
22
+ };
23
+ const parentSessionId = link('parentSessionId');
24
+ const rootSessionId = link('rootSessionId');
25
+ return Object.freeze({
26
+ ...(parentSessionId !== undefined ? { parentSessionId } : {}),
27
+ ...(rootSessionId !== undefined ? { rootSessionId } : {}),
28
+ });
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
@@ -1,4 +1,4 @@
1
- import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleBehavior, RuleListParams, RuleListResult, RuleRevokeRequest, RemovalLiveness, RuleRevokeResult, RuleWriteRequest, RuleWriteResult } from '@sema-agent/sdk';
1
+ import type { CcImportLayer, CcImportPrepareResult, PersistedRule, RuleBehavior, RuleListParams, RuleListResult, RuleRevokeRequest, RemovalLiveness, RuleRevokeResult, RuleWriteRequest, RuleWriteResult, RulesResource } from '@sema-agent/sdk';
2
2
  import type { UiLanguage } from '../env/uiLanguage.js';
3
3
  export interface RulesFacade {
4
4
  list(params?: RuleListParams, opts?: {
@@ -15,17 +15,29 @@ export interface RulesFacade {
15
15
  }): Promise<CcImportPrepareResult>;
16
16
  ccImportRedeem(ticket: string, opts?: {
17
17
  signal?: AbortSignal;
18
- }): Promise<CcImportRedeemResult>;
18
+ }): ReturnType<RulesResource['ccImportRedeem']>;
19
19
  }
20
20
  export type RulesListFacade = Pick<RulesFacade, 'list'>;
21
21
  export declare function persistedRulesLaneAvailable(baseUrl: string | undefined): boolean;
22
22
  export declare function persistedRulesGovernanceAvailable(baseUrl: string | undefined): boolean;
23
23
  export declare function persistedRulesWriteAvailable(baseUrl: string | undefined): boolean;
24
+ export interface CcImportRedeemIndeterminate {
25
+ readonly key?: CcImportRedeemRuleKey;
26
+ readonly reason?: string;
27
+ readonly detail?: string;
28
+ }
29
+ export interface CcImportRedeemRuleKey {
30
+ readonly behavior: string;
31
+ readonly rule: string;
32
+ readonly scope: string;
33
+ }
24
34
  export interface CcImportRedeemCounts {
25
35
  persisted: number;
26
36
  deduped: number;
37
+ indeterminate: readonly CcImportRedeemIndeterminate[];
27
38
  }
28
- export declare function readCcImportRedeemCounts(result: unknown): CcImportRedeemCounts | null;
39
+ export type CcImportReceiptOutcome = 'persisted' | 'no-op' | 'indeterminate';
40
+ export declare function readCcImportRedeemCounts(body: unknown): CcImportRedeemCounts | null;
29
41
  export type RulesFailure = {
30
42
  kind: 'lane-unavailable';
31
43
  message: string;
@@ -1,11 +1,11 @@
1
1
  import { displayUntrusted, quoteUntrustedForSentence } from '../displayUntrusted.js';
2
2
  import { engineCapTrue } from '../engineCapsCache.js';
3
3
  import { CORE_RULE_BEHAVIORS } from '../generated/engineFactTables.js';
4
- import { wireFailureShapeOf } from '../wireFailureShape.js';
4
+ import { slotReadOf, wireFailureShapeOf } from '../wireFailureShape.js';
5
5
  import { CAPABILITY_OPERATOR_PRINCIPALS_REQUIRED } from '../engineErrorCodes.js';
6
6
  import { hostLog } from '../host.js';
7
7
  import { msg, renderMessage } from '../i18n/translate.js';
8
- import { guardedRead, readOwnDataValue } from '../ownKey.js';
8
+ import { appendOwnElement, guardedRead, readOwnDataValue } from '../ownKey.js';
9
9
  import { observedPermissionRulesWrite, permissionRulesWriteAvailable } from '../permissionRulesWriteCapability.js';
10
10
  import { isToolApprovalRespondReplayed, readToolApprovalRespondAck } from './toolApprovalWire.js';
11
11
  export function persistedRulesLaneAvailable(baseUrl) {
@@ -17,27 +17,109 @@ export function persistedRulesGovernanceAvailable(baseUrl) {
17
17
  export function persistedRulesWriteAvailable(baseUrl) {
18
18
  return permissionRulesWriteAvailable(observedPermissionRulesWrite(baseUrl ?? ''));
19
19
  }
20
- export function readCcImportRedeemCounts(result) {
21
- const o = (result ?? {});
22
- if (Array.isArray(o.members)) {
23
- let persisted = 0;
24
- let deduped = 0;
25
- for (const m of o.members) {
26
- const status = m?.status;
27
- if (status === 'persisted')
28
- persisted += 1;
29
- else if (status === 'deduped')
30
- deduped += 1;
20
+ const NO_INDETERMINATE = Object.freeze([]);
21
+ const INDETERMINATE_DETAIL_SHOWN_MAX = 400;
22
+ function ownArrayItems(v) {
23
+ try {
24
+ if (typeof v !== 'object' || v === null || !Array.isArray(v))
25
+ return undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ const len = slotReadOf(v, 'length');
31
+ if (len.presence !== 'present' || typeof len.value !== 'number' || !Number.isSafeInteger(len.value) || len.value < 0)
32
+ return undefined;
33
+ const out = [];
34
+ for (let i = 0; i < len.value; i++) {
35
+ const item = slotReadOf(v, String(i));
36
+ if (item.presence !== 'present')
37
+ return undefined;
38
+ appendOwnElement(out, item.value);
39
+ }
40
+ return out;
41
+ }
42
+ function ownNonEmpty(o, key) {
43
+ const r = slotReadOf(o, key);
44
+ return r.presence === 'present' && typeof r.value === 'string' && r.value !== '' ? r.value : undefined;
45
+ }
46
+ function indeterminateRowOf(row) {
47
+ const k = slotReadOf(row, 'key');
48
+ const keyObj = k.presence === 'present' && typeof k.value === 'object' && k.value !== null ? k.value : undefined;
49
+ const behavior = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'behavior');
50
+ const rule = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'rule');
51
+ const scope = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'scope');
52
+ const reason = ownNonEmpty(row, 'reason');
53
+ const d = slotReadOf(row, 'detail');
54
+ const shown = d.presence === 'present' && typeof d.value === 'string' ? displayUntrusted(d.value, { max: INDETERMINATE_DETAIL_SHOWN_MAX }) : undefined;
55
+ return Object.freeze({
56
+ ...(behavior !== undefined && rule !== undefined && scope !== undefined ? { key: Object.freeze({ behavior, rule, scope }) } : {}),
57
+ ...(reason !== undefined ? { reason } : {}),
58
+ ...(shown !== undefined && shown.trim() !== '' ? { detail: shown } : {}),
59
+ });
60
+ }
61
+ const RECEIPT_ROW_ARMS = {
62
+ persisted: (acc) => {
63
+ acc.persisted += 1;
64
+ },
65
+ 'no-op': (acc) => {
66
+ acc.deduped += 1;
67
+ },
68
+ indeterminate: (acc, row) => {
69
+ appendOwnElement(acc.indeterminate, indeterminateRowOf(row));
70
+ },
71
+ };
72
+ function receiptCountsOf(rows) {
73
+ const acc = { persisted: 0, deduped: 0, indeterminate: [] };
74
+ for (const row of rows) {
75
+ if (typeof row !== 'object' || row === null)
76
+ return null;
77
+ const o = slotReadOf(row, 'outcome');
78
+ const word = o.presence === 'present' ? o.value : undefined;
79
+ if (typeof word !== 'string' || !Object.hasOwn(RECEIPT_ROW_ARMS, word))
80
+ return null;
81
+ RECEIPT_ROW_ARMS[word](acc, row);
82
+ }
83
+ return { persisted: acc.persisted, deduped: acc.deduped, indeterminate: acc.indeterminate.length === 0 ? NO_INDETERMINATE : Object.freeze(acc.indeterminate) };
84
+ }
85
+ function legacyEnvelopeCountsOf(result) {
86
+ if (typeof result !== 'object' || result === null)
87
+ return null;
88
+ const m = slotReadOf(result, 'members');
89
+ const rows = m.presence === 'present' ? ownArrayItems(m.value) : undefined;
90
+ if (rows === undefined)
91
+ return null;
92
+ let persisted = 0;
93
+ let deduped = 0;
94
+ for (const row of rows) {
95
+ if (typeof row !== 'object' || row === null)
96
+ continue;
97
+ const st = slotReadOf(row, 'status');
98
+ const status = st.presence === 'present' ? st.value : undefined;
99
+ if (status === 'persisted')
100
+ persisted += 1;
101
+ else if (status === 'deduped')
102
+ deduped += 1;
103
+ }
104
+ return { persisted, deduped, indeterminate: NO_INDETERMINATE };
105
+ }
106
+ export function readCcImportRedeemCounts(body) {
107
+ try {
108
+ if (typeof body !== 'object' || body === null)
109
+ return null;
110
+ const top = slotReadOf(body, 'members');
111
+ if (top.presence !== 'absent') {
112
+ if (top.presence !== 'present')
113
+ return null;
114
+ const rows = ownArrayItems(top.value);
115
+ return rows === undefined ? null : receiptCountsOf(rows);
31
116
  }
32
- return { persisted, deduped };
117
+ const legacy = slotReadOf(body, 'result');
118
+ return legacy.presence === 'present' ? legacyEnvelopeCountsOf(legacy.value) : null;
33
119
  }
34
- if (Array.isArray(o.persisted) || Array.isArray(o.deduped)) {
35
- return {
36
- persisted: Array.isArray(o.persisted) ? o.persisted.length : 0,
37
- deduped: Array.isArray(o.deduped) ? o.deduped.length : 0,
38
- };
120
+ catch {
121
+ return null;
39
122
  }
40
- return null;
41
123
  }
42
124
  export function classifyRulesFailure(e, context) {
43
125
  const { status, errorCode, message, retryAfterSec, committed } = wireFailureShapeOf(e);
@@ -677,6 +677,8 @@ function planReviewSettledVerdict(r) {
677
677
  return status !== 'completed' && !gateMoved ? 'still_parked' : 'unconfirmed';
678
678
  if (status === 'completed')
679
679
  return 'took_effect';
680
+ if (status === 'running' && r.decision === 'reject')
681
+ return 'took_effect';
680
682
  const newGateHasCard = () => {
681
683
  const leg = durableDecideLegOf({ gateKind: r.gateKindAfter ?? null });
682
684
  return leg === 'approval' || leg === 'plan_review';
@@ -874,7 +876,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
874
876
  const bodyStatus = bodyWordOf(body, 'status');
875
877
  const effective = postStatus ?? bodyStatus;
876
878
  statusSeen = effective;
877
- effect = planReviewSettledVerdict({ postStatus, bodyStatus, reparked: bodySaysReparked(body), gateBefore, gateAfter, gateKindAfter });
879
+ effect = planReviewSettledVerdict({ decision, postStatus, bodyStatus, reparked: bodySaysReparked(body), gateBefore, gateAfter, gateKindAfter });
878
880
  const code = bodyWordOf(body, 'errorCode');
879
881
  if (effect === 'still_parked' && postStatus === 'needs_review' && !bodySaysReparked(body)) {
880
882
  outcome =
@@ -892,7 +894,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
892
894
  }
893
895
  else if (effect === 'took_effect') {
894
896
  outcome =
895
- `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: ${effective} ` +
897
+ `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — ${effective === 'completed' ? 'final status' : 'status'}: ${effective} ` +
896
898
  `(${postStatus !== undefined ? 'post-decide re-checked: task left the review gate' : "from the decision's own reply; the post-decide re-check was unavailable"}).`;
897
899
  }
898
900
  else if (effective === 'needs_review' && (gateBefore === undefined || gateAfter === undefined || postStatus !== 'needs_review')) {
@@ -1,2 +1,7 @@
1
1
  export type SessionAllowLeg = 'frame' | 'v1' | 'generic';
2
2
  export declare function sessionAllowCarried(leg: SessionAllowLeg, approvalId: unknown): boolean;
3
+ export type SessionAllowWhy = 'given' | 'route_cannot_carry' | 'evidence_unreadable' | 'card_unreadable';
4
+ export type CardUnreadableEvidence = readonly string[] | 'absent' | 'unreadable';
5
+ export declare function readCardUnreadableEvidence(card: object): CardUnreadableEvidence;
6
+ export declare function sessionAllowCardReadable(card: object | null | undefined): Exclude<SessionAllowWhy, 'route_cannot_carry'>;
7
+ export declare function sessionAllowEligible(card: object | null | undefined): SessionAllowWhy;
@@ -5,3 +5,45 @@ export function sessionAllowCarried(leg, approvalId) {
5
5
  return typeof approvalId === 'string' && approvalId !== '';
6
6
  return false;
7
7
  }
8
+ export function readCardUnreadableEvidence(card) {
9
+ try {
10
+ if (!('unreadableEvidence' in card))
11
+ return 'absent';
12
+ const d = Object.getOwnPropertyDescriptor(card, 'unreadableEvidence');
13
+ const list = d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
14
+ return Array.isArray(list) && list.every((k) => typeof k === 'string') ? list : 'unreadable';
15
+ }
16
+ catch {
17
+ return 'unreadable';
18
+ }
19
+ }
20
+ export function sessionAllowCardReadable(card) {
21
+ if (typeof card !== 'object' || card === null)
22
+ return 'card_unreadable';
23
+ try {
24
+ if ('cardPayloadUnreadable' in card)
25
+ return 'card_unreadable';
26
+ }
27
+ catch {
28
+ return 'card_unreadable';
29
+ }
30
+ const evidence = readCardUnreadableEvidence(card);
31
+ if (evidence === 'unreadable')
32
+ return 'card_unreadable';
33
+ return evidence !== 'absent' && evidence.length > 0 ? 'evidence_unreadable' : 'given';
34
+ }
35
+ export function sessionAllowEligible(card) {
36
+ if (typeof card !== 'object' || card === null)
37
+ return 'card_unreadable';
38
+ let carried;
39
+ try {
40
+ const d = Object.getOwnPropertyDescriptor(card, 'allowSessionCarried');
41
+ carried = d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
42
+ }
43
+ catch {
44
+ return 'card_unreadable';
45
+ }
46
+ if (carried !== true)
47
+ return 'route_cannot_carry';
48
+ return sessionAllowCardReadable(card);
49
+ }
@@ -4,6 +4,7 @@ import { readApprovalCardWord, readRuleOffers } from './toolApprovalWire.js';
4
4
  import { CORE_SKILL_TOOL_NAME } from '../generated/toolNameTables.js';
5
5
  import { toolPermissionRequestIdDomain } from '../seatContract.js';
6
6
  import { permissionsDeclarationReport } from '../permissionsDeclaration.js';
7
+ import { readCardUnreadableEvidence, sessionAllowCardReadable, sessionAllowEligible } from './sessionAllowRoute.js';
7
8
  const UNREADABLE = Symbol('unreadable');
8
9
  function ownData(v, key) {
9
10
  try {
@@ -90,18 +91,17 @@ function enginePremiseOf(card) {
90
91
  return 'card_unreadable';
91
92
  if (refused.inCard)
92
93
  return refused.value === true ? 'remember_refused' : 'remember_unreadable';
93
- const evidence = present(card, 'unreadableEvidence');
94
- if (evidence === UNREADABLE)
94
+ const list = readCardUnreadableEvidence(card);
95
+ if (list === 'unreadable')
95
96
  return 'card_unreadable';
96
- if (!evidence.inCard)
97
+ if (list === 'absent')
97
98
  return { evidenceUnreadable: false };
98
- const list = ownData(card, 'unreadableEvidence');
99
- if (!Array.isArray(list) || !list.every((k) => typeof k === 'string'))
100
- return 'card_unreadable';
101
99
  if (list.includes('rememberRefused'))
102
100
  return 'remember_unreadable';
103
101
  return { evidenceUnreadable: list.length > 0 };
104
102
  }
103
+ const _sessionWhyPin = true;
104
+ void _sessionWhyPin;
105
105
  export function standingRowsOf(card, host) {
106
106
  if (typeof card !== 'object' || card === null)
107
107
  return allClosed('card_unreadable');
@@ -110,11 +110,11 @@ export function standingRowsOf(card, host) {
110
110
  if (typeof premise === 'string')
111
111
  return allClosed(premise);
112
112
  const evidenceUnreadable = premise.evidenceUnreadable;
113
- const sessionRow = ownData(card, 'allowSessionCarried') === true;
114
- const sessionWhy = sessionRow ? 'given' : 'route_cannot_carry';
113
+ const sessionWhy = sessionAllowEligible(card);
114
+ const sessionRow = sessionWhy === 'given';
115
115
  let deploymentList;
116
116
  if (!sessionRow) {
117
- deploymentList = 'route_cannot_carry';
117
+ deploymentList = sessionWhy;
118
118
  }
119
119
  else {
120
120
  const pair = readAskClassPairState(card);
@@ -143,7 +143,7 @@ export function seatSessionRowOf(card, requestId) {
143
143
  const domain = toolPermissionRequestIdDomain(requestId);
144
144
  if (domain !== 'live:' && domain !== 'durable:')
145
145
  return false;
146
- return typeof enginePremiseOf(card) !== 'string';
146
+ return typeof enginePremiseOf(card) !== 'string' && sessionAllowCardReadable(card) === 'given';
147
147
  }
148
148
  catch {
149
149
  return false;
@@ -15,7 +15,7 @@ import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
15
15
  import { CORE_SKILL_TOOL_NAME } from '../generated/toolNameTables.js';
16
16
  import { CORE_ALLOW_ENTRY_INERT_REASONS, CORE_PERSISTED_RULE_MATCHES } from '../generated/engineFactTables.js';
17
17
  import { msg, renderMessage } from '../i18n/translate.js';
18
- import { sessionAllowCarried } from './sessionAllowRoute.js';
18
+ import { sessionAllowCarried, sessionAllowEligible } from './sessionAllowRoute.js';
19
19
  import { INTERRUPT_DENY_REASON, observeCancelByDeny, surfaceApprovalParking, surfaceRememberNotApplied, surfaceEditNotForwarded, surfaceEditRefusedOnBlindAsk, surfaceRuleArmNotSent, surfaceRuleArmRejected } from './hitlHostSurface.js';
20
20
  import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
21
21
  import { displayUntrusted, quoteUntrustedForSentence } from '../displayUntrusted.js';
@@ -449,7 +449,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
449
449
  closeDurableRead(readMark);
450
450
  }
451
451
  if (!found.ok)
452
- return { kind: 'not_sent', lane: 'decide', cause: found.code ?? 'fetch_failed', ...(found.reason !== '' ? { detail: found.reason } : {}) };
452
+ return { kind: 'not_sent', lane: 'decide', cause: found.code ?? 'fetch_failed', ...(found.reason !== '' ? { detail: found.reason } : {}), ...(found.rowVerdict !== undefined ? { rowVerdict: found.rowVerdict } : {}) };
453
453
  if (held !== undefined)
454
454
  return joinDurableDecision(held, deps, signal);
455
455
  const rowIdentity = parkRowHolderIdentity(found.pending, found.gatedCallId);
@@ -500,7 +500,7 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
500
500
  const durableRuleOffersAbsence = pending.ruleOffersAbsence;
501
501
  hold.presented = true;
502
502
  deps.onPresented?.();
503
- const card = await surfaceCallTrackedCard(gatedCallId, undefined, ledgerSlot, withUnreadableEvidence(pending, 'row', {
503
+ const cardRequest = withUnreadableEvidence(pending, 'row', {
504
504
  toolName,
505
505
  args,
506
506
  callKey,
@@ -533,7 +533,8 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
533
533
  allowSessionCarried: true,
534
534
  grantBinding: readApprovalGrantBinding(pending),
535
535
  parkRow: parkRowOf(taskId, pending, gatedCallId),
536
- }));
536
+ });
537
+ const card = await surfaceCallTrackedCard(gatedCallId, undefined, ledgerSlot, cardRequest);
537
538
  hold.phase = 'respond';
538
539
  if (rowArgsGap !== undefined && card.kind === 'allow' && card.updatedInput !== undefined) {
539
540
  hostLog('error', `liveToolApprovalWire: approval card for ${gatedCallId ?? `task ${taskId}`} returned an edited approval, but the tool input is ${rowArgsGap === 'truncated' ? 'a size marker on the pending row' : 'missing from both the stream and the pending row'} — nothing sent (the park stays pending)`);
@@ -560,10 +561,15 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
560
561
  let rememberStop;
561
562
  try {
562
563
  let rememberRefused = false;
563
- const approveBody = answerPayloadFor(card, 'decide');
564
+ const sessionAllowWhy = sessionAllowEligible(cardRequest);
565
+ const sessionDowngraded = card.allowSession === true && sessionAllowWhy !== 'given';
566
+ if (sessionDowngraded)
567
+ hostLog('debug', `liveToolApprovalWire: ${gatedCallId ?? `task ${taskId}`} session-wide allow taken as allow-once — the card is not eligible (${sessionAllowWhy})`);
568
+ const answered = sessionDowngraded ? { ...card, allowSession: false } : card;
569
+ const approveBody = answerPayloadFor(answered, 'decide');
564
570
  const { remember: _remember, ...plainApproveBody } = approveBody;
565
571
  void _remember;
566
- emitAnswerCommitted(callKey, 'decide', card, approveBody);
572
+ emitAnswerCommitted(callKey, 'decide', answered, approveBody);
567
573
  if (approveBody.remember === 'session') {
568
574
  let sessionReceipt;
569
575
  let sessionDecided = false;
@@ -591,7 +597,7 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
591
597
  }
592
598
  const raw = await bridge.decideTool(plainApproveBody, gatedCallId, decideOpts, pending);
593
599
  const receipt = readDecideReceipt(raw);
594
- if (rememberRefused)
600
+ if (rememberRefused || sessionDowngraded)
595
601
  surfaceRememberNotApplied();
596
602
  return { kind: 'decided', ...at, decision: 'allow', ...(receipt !== undefined ? { receipt } : {}) };
597
603
  }
@@ -2038,9 +2044,15 @@ async function presentToolApprovalFrameAndRespond(frame, carriers, approvalId, r
2038
2044
  enterLiveRespondPhase(hold);
2039
2045
  const noteCapable = lane?.approvalDecisionNoteCapable === true;
2040
2046
  const denyReasonRead = card.kind === 'deny' ? ((r) => (typeof r === 'string' ? r : undefined))(card.reason) : undefined;
2047
+ const sessionAllowWhy = sessionAllowEligible(facts);
2048
+ const sessionDowngraded = card.kind === 'allow' && card.allowSession === true && sessionAllowWhy !== 'given';
2049
+ if (sessionDowngraded)
2050
+ hostLog('debug', `liveToolApprovalWire: ${approvalId} session-wide allow taken as allow-once — the card is not eligible (${sessionAllowWhy})`);
2041
2051
  const answerSnapshot = card.kind === 'deny'
2042
2052
  ? ((by) => ({ kind: 'deny', ...(denyReasonRead !== undefined ? { reason: denyReasonRead } : {}), ...(by !== undefined ? { settledBy: by } : {}) }))(readApprovalDenySettledBy(card))
2043
- : card;
2053
+ : sessionDowngraded && card.kind === 'allow'
2054
+ ? { ...card, allowSession: false }
2055
+ : card;
2044
2056
  const basePayload = answerPayloadFor(answerSnapshot, 'respond', { approvalDecisionNoteCapable: noteCapable });
2045
2057
  const decision = basePayload?.decision ?? 'deny';
2046
2058
  if (card.kind === 'allow' && typeof card.note === 'string' && card.note.trim() !== '' && answerCarriage({ callKey: liveFrameCallKey(approvalId) }).allowNote === 'none') {
@@ -2273,6 +2285,8 @@ async function presentToolApprovalFrameAndRespond(frame, carriers, approvalId, r
2273
2285
  logAfterDecided(`liveToolApprovalWire: ${approvalId} allow_session ack rememberApplied=false — grant not stored, surfacing honest notice`);
2274
2286
  surfaceRememberNotApplied();
2275
2287
  }
2288
+ if (replayed === undefined && sessionDowngraded)
2289
+ surfaceRememberNotApplied();
2276
2290
  if (respondOpts.updatedInput !== undefined && applied?.updatedInputForwarded === false) {
2277
2291
  logAfterDecided(`liveToolApprovalWire: ${approvalId} edited args were NOT forwarded (ack.updatedInputForwarded=false) — the tool runs on the ORIGINAL input`);
2278
2292
  surfaceEditNotForwarded();
@@ -247,6 +247,10 @@ declare const EN: {
247
247
  'approval.settle.cause.streamDisconnected': string;
248
248
  'approval.settle.cause.undeliverable': string;
249
249
  'approval.decide.requiresBinding': string;
250
+ 'approval.decide.unboundWayOut': string;
251
+ 'approval.rowUnresumable.fallback': string;
252
+ 'approval.rowUnresumable.askNotSurfaced': string;
253
+ 'approval.park.stillParked': string;
250
254
  'approval.decision.grantChanged': string;
251
255
  'approval.decision.settledElsewhereContrary.deniedFirst': string;
252
256
  'approval.decision.settledElsewhereContrary.approvedFirst': string;
@@ -422,6 +426,7 @@ declare const EN: {
422
426
  'session.selfHeal.row.askDecided.inferred': string;
423
427
  'session.selfHeal.row.askDecided.releaseUnknown': string;
424
428
  'session.selfHeal.row.askReopenFailed.withKeep': string;
429
+ 'session.selfHeal.row.askReopenFailed.rowUnresumable': string;
425
430
  'session.selfHeal.row.askReopenFailed.noKeep': string;
426
431
  'session.selfHeal.row.planReopenFailed.dismissed': string;
427
432
  'session.selfHeal.row.planReopenFailed.withKeep': string;
@@ -983,6 +988,7 @@ declare const EN: {
983
988
  'notice.wireRefusal.hostPosture.both': string;
984
989
  'notice.wireRefusal.hostPosture.unattributed': string;
985
990
  'notice.wireRefusal.autonomyLane': string;
991
+ 'notice.wireRefusal.sessionArchiveStamped': string;
986
992
  'notice.wireRefusal.unsupportedRule': string;
987
993
  'notice.wireRefusal.unsupportedRule.other': string;
988
994
  'notice.wireRefusal.unsupportedRule.stored': string;