@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
@@ -3,20 +3,21 @@ import { displayUntrusted } from './displayUntrusted.js';
3
3
  import { CORE_ENGINE_NOTICE_CODES, CORE_MCP_INJECTION_DROP_REASONS, CORE_NOTICE_AUDIENCE, CORE_USER_INPUT_UNDRAINED_FAMILIES, } from './generated/engineNoticeTables.js';
4
4
  import { CORE_APPROVAL_REMEDY_KINDS, CORE_ASK_UNRESOLVABLE_CAUSES, CORE_DURABLE_MANDATE_SOURCES, CORE_PERMISSION_MODE_FOLD_REASONS, } from './generated/engineFactTables.js';
5
5
  import { msg, renderMessage } from './i18n/translate.js';
6
- import { readOwnDataValue } from './ownKey.js';
6
+ import { ownDataField, readOwnDataValue } from './ownKey.js';
7
7
  export const ENGINE_NOTICE_CODES = CORE_ENGINE_NOTICE_CODES;
8
8
  const CATALOG = new Set(ENGINE_NOTICE_CODES);
9
9
  function audienceRow(table, code) {
10
10
  return Object.hasOwn(table, code) ? table[code] : undefined;
11
11
  }
12
+ const LEGACY_NOTICE_AUDIENCE = Object.freeze({ 'context.image_removed': 'user', 'session.records_skipped': 'user' });
12
13
  export function engineNoticeInCatalog(code) {
13
- return typeof code === 'string' && CATALOG.has(code);
14
+ return typeof code === 'string' && (CATALOG.has(code) || Object.hasOwn(LEGACY_NOTICE_AUDIENCE, code));
14
15
  }
15
16
  export const ENGINE_NOTICE_AUDIENCE = CORE_NOTICE_AUDIENCE;
16
17
  export function noticeAudienceOf(code) {
17
18
  if (typeof code !== 'string')
18
19
  return 'operator';
19
- return audienceRow(ENGINE_NOTICE_AUDIENCE, code) ?? 'operator';
20
+ return audienceRow(ENGINE_NOTICE_AUDIENCE, code) ?? audienceRow(LEGACY_NOTICE_AUDIENCE, code) ?? 'operator';
20
21
  }
21
22
  export const MCP_INJECTION_DROP_REASONS = CORE_MCP_INJECTION_DROP_REASONS;
22
23
  const DROP_REASONS = new Set(MCP_INJECTION_DROP_REASONS);
@@ -536,6 +537,15 @@ function optionalOwnSeat(o, key) {
536
537
  return undefined;
537
538
  }
538
539
  }
540
+ const UNREADABLE = Symbol('unreadable');
541
+ function ownSeatOrUnreadable(o, key) {
542
+ try {
543
+ return ownSeat(o, key);
544
+ }
545
+ catch {
546
+ return UNREADABLE;
547
+ }
548
+ }
539
549
  function readToolTurnRepairedOf(notice) {
540
550
  if (typeof notice !== 'object' || notice === null || Array.isArray(notice))
541
551
  return undefined;
@@ -716,7 +726,10 @@ function readUserInputUndrainedOf(notice) {
716
726
  const taskId = nonEmpty(optionalOwnSeat(o, 'taskId'));
717
727
  const sessionId = nonEmpty(optionalOwnSeat(o, 'sessionId'));
718
728
  const runId = nonEmpty(optionalOwnSeat(o, 'runId'));
719
- const childSessionId = nonEmpty(optionalOwnSeat(o, 'childSessionId'));
729
+ const childSeat = ownSeatOrUnreadable(o, 'childSessionId');
730
+ const childSessionId = nonEmpty(childSeat);
731
+ const childSeatProven = childSeat === undefined || childSessionId !== undefined;
732
+ const inputIds = childSeatProven ? exactInputIdList(optionalOwnSeat(o, 'inputIds'), count) : undefined;
720
733
  return {
721
734
  kind,
722
735
  count,
@@ -725,8 +738,96 @@ function readUserInputUndrainedOf(notice) {
725
738
  ...(sessionId !== undefined ? { sessionId } : {}),
726
739
  ...(runId !== undefined ? { runId } : {}),
727
740
  ...(childSessionId !== undefined ? { childSessionId } : {}),
741
+ ...(inputIds !== undefined ? { inputIds } : {}),
728
742
  };
729
743
  }
744
+ function exactInputIdList(v, count) {
745
+ try {
746
+ if (!Array.isArray(v) || ownDataField(v, 'length') !== count)
747
+ return undefined;
748
+ const out = [];
749
+ const seen = new Set();
750
+ for (let i = 0; i < count; i++) {
751
+ const d = Object.getOwnPropertyDescriptor(v, i);
752
+ const id = d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
753
+ if (typeof id !== 'string' || id.length === 0 || seen.has(id))
754
+ return undefined;
755
+ seen.add(id);
756
+ out.push(id);
757
+ }
758
+ return Object.freeze(out);
759
+ }
760
+ catch {
761
+ return undefined;
762
+ }
763
+ }
764
+ function strictSeat(o, key) {
765
+ try {
766
+ return ownDataField(o, key);
767
+ }
768
+ catch {
769
+ return UNREADABLE;
770
+ }
771
+ }
772
+ function ledgerIdSet(ledger, key) {
773
+ const list = strictSeat(ledger, key);
774
+ if (list === undefined)
775
+ return new Set();
776
+ try {
777
+ if (!Array.isArray(list))
778
+ return UNREADABLE;
779
+ const out = new Set();
780
+ for (const k of Object.getOwnPropertyNames(list)) {
781
+ if (k === 'length' || String(Number(k) >>> 0) !== k || Number(k) === 4294967295)
782
+ continue;
783
+ const id = strictSeat(list, k);
784
+ if (id === UNREADABLE)
785
+ return UNREADABLE;
786
+ if (typeof id === 'string' && id.length > 0)
787
+ out.add(id);
788
+ }
789
+ return out;
790
+ }
791
+ catch {
792
+ return UNREADABLE;
793
+ }
794
+ }
795
+ export function undrainedInputsVerdict(facts, ledger) {
796
+ if (facts === null || typeof facts !== 'object')
797
+ return undefined;
798
+ try {
799
+ if (Array.isArray(facts))
800
+ return undefined;
801
+ }
802
+ catch {
803
+ return undefined;
804
+ }
805
+ const count = positiveInt(strictSeat(facts, 'count'));
806
+ if (count === undefined)
807
+ return undefined;
808
+ const nothing = Object.freeze({ redeliver: Object.freeze([]), askUser: Object.freeze([]), unattributed: count });
809
+ if (strictSeat(facts, 'cause') !== 'leg_end' || strictSeat(facts, 'childSessionId') !== undefined)
810
+ return nothing;
811
+ const ids = exactInputIdList(strictSeat(facts, 'inputIds'), count);
812
+ if (ids === undefined)
813
+ return nothing;
814
+ if (ledger !== null && ledger !== undefined && typeof ledger !== 'object')
815
+ return nothing;
816
+ const l = ledger ?? {};
817
+ const handed = ledgerIdSet(l, 'handed');
818
+ const redelivered = ledgerIdSet(l, 'redelivered');
819
+ if (handed === UNREADABLE || redelivered === UNREADABLE)
820
+ return nothing;
821
+ const redeliver = [];
822
+ const askUser = [];
823
+ for (const id of ids) {
824
+ if (redelivered.has(id))
825
+ askUser.push(id);
826
+ else if (handed.has(id))
827
+ redeliver.push(id);
828
+ }
829
+ return Object.freeze({ redeliver: Object.freeze(redeliver), askUser: Object.freeze(askUser), unattributed: count - redeliver.length - askUser.length });
830
+ }
730
831
  const messagesPhrase = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
731
832
  const verbBe = (n) => (n === 1 ? 'was' : 'were');
732
833
  const itThem = (n) => (n === 1 ? 'it' : 'them');
@@ -904,18 +1005,27 @@ function readContextMediaRemovedOf(notice) {
904
1005
  if (typeof notice !== 'object' || notice === null || Array.isArray(notice))
905
1006
  return undefined;
906
1007
  const n = notice;
907
- if (ownSeat(n, 'code') !== 'context.media_removed')
1008
+ const code = ownSeat(n, 'code');
1009
+ if (code !== 'context.media_removed' && code !== 'context.image_removed')
908
1010
  return undefined;
909
1011
  const d = ownSeat(n, 'detail');
910
1012
  if (typeof d !== 'object' || d === null || Array.isArray(d))
911
1013
  return undefined;
912
1014
  const o = d;
1015
+ const count = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
1016
+ if (code === 'context.image_removed') {
1017
+ const legacyImages = ownSeat(o, 'images');
1018
+ const legacySessionId = nonEmpty(ownSeat(o, 'sessionId'));
1019
+ const legacyRunId = nonEmpty(ownSeat(o, 'runId'));
1020
+ if (!count(legacyImages) || legacyImages < 1 || legacySessionId === undefined || legacyRunId === undefined)
1021
+ return undefined;
1022
+ return { reason: 'rejected_by_provider', images: legacyImages, documents: 0, sessionId: legacySessionId, runId: legacyRunId };
1023
+ }
913
1024
  const reason = nonEmpty(ownSeat(o, 'reason'));
914
1025
  const images = ownSeat(o, 'images');
915
1026
  const documents = ownSeat(o, 'documents');
916
1027
  const sessionId = nonEmpty(ownSeat(o, 'sessionId'));
917
1028
  const runId = nonEmpty(ownSeat(o, 'runId'));
918
- const count = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
919
1029
  if (reason === undefined || !count(images) || !count(documents) || images + documents < 1 || sessionId === undefined || runId === undefined)
920
1030
  return undefined;
921
1031
  return { reason, images, documents, sessionId, runId };
@@ -1009,6 +1119,7 @@ export const ENGINE_NOTICE_FACT_READERS = Object.freeze({
1009
1119
  'route.stream_end_ignored': readStreamEndIgnored,
1010
1120
  'mcp.authorization_required': readMcpAuthorizationRequired,
1011
1121
  'context.media_removed': readContextMediaRemoved,
1122
+ 'context.image_removed': readContextMediaRemoved,
1012
1123
  'session.lineage_rerooted': readSessionLineageRerooted,
1013
1124
  });
1014
1125
  const _factReaderTablePin = true;
@@ -5,7 +5,7 @@ import { resumeReopenContent } from './hitl/suspendedReopen.js';
5
5
  import { idempotencyPayloadMismatchContent } from './engineErrorCodes.js';
6
6
  import { inheritEnvRefusalContent } from './inheritEnvWire.js';
7
7
  import { projectSeedRefusalContent } from './projectSeed.js';
8
- import { usageWindowExhaustedContent, fleetLeaseUnavailableContent, settingsPermissionBucketRefusalContent, fieldLimitRefusalContent, autonomyLaneRefusalContent, unsupportedRuleRefusalContent, admissionWallRefusalContent, hostPostureRefusalContent, toolDeclarationRefusalContent, } from './wireRefusalCopy.js';
8
+ import { usageWindowExhaustedContent, fleetLeaseUnavailableContent, settingsPermissionBucketRefusalContent, fieldLimitRefusalContent, autonomyLaneRefusalContent, sessionArchiveStampedContent, unsupportedRuleRefusalContent, admissionWallRefusalContent, hostPostureRefusalContent, toolDeclarationRefusalContent, } from './wireRefusalCopy.js';
9
9
  const ROLE_BY_READER = new Map([
10
10
  [corruptSessionContent, 'replacement'],
11
11
  [corruptStoredRowContent, 'replacement'],
@@ -19,6 +19,7 @@ const ROLE_BY_READER = new Map([
19
19
  [settingsPermissionBucketRefusalContent, 'replacement'],
20
20
  [fieldLimitRefusalContent, 'replacement'],
21
21
  [autonomyLaneRefusalContent, 'replacement'],
22
+ [sessionArchiveStampedContent, 'replacement'],
22
23
  [idempotencyPayloadMismatchContent, 'replacement'],
23
24
  [unsupportedRuleRefusalContent, 'replacement'],
24
25
  [projectSeedRefusalContent, 'replacement'],
@@ -2,6 +2,7 @@ import { capForDisplay } from './fleetTaskDesc.js';
2
2
  import { engineWireTargetBaseUrl } from './engineWireTarget.js';
3
3
  import { createEngineCapReader, unreadableReadingDetail } from './engineCapReader.js';
4
4
  import { ownDataField } from './ownKey.js';
5
+ import { executionLaneWordIsNone } from './executionLaneWords.js';
5
6
  export function projectExecutionLaneCapability(caps) {
6
7
  try {
7
8
  if (caps === null || typeof caps !== 'object')
@@ -100,8 +101,8 @@ export function executionLaneDoctorDetail(reading) {
100
101
  if (reading.view.toolsOnThisHost) {
101
102
  return `execution lane ${lane} — tools run on the engine host (same filesystem), so skills that have a directory of their own can carry a baseDir`;
102
103
  }
103
- if (reading.view.provider === 'in-process') {
104
- return 'no execution lane on this deployment (in-process) — the engine has no machine of its own for running tools here, so its built-in file and shell tools are not mounted and skill baseDir is never sent; whether a given run had tools is answered by its tool roster';
104
+ if (executionLaneWordIsNone(reading.view.provider)) {
105
+ return `no execution lane on this deployment (${lane}) — the engine has no machine of its own for running tools here, so its built-in file and shell tools are not mounted and skill baseDir is never sent; whether a given run had tools is answered by its tool roster`;
105
106
  }
106
107
  return `execution lane ${lane} — tools run on a different machine than the engine, so skill baseDir is never sent on this deployment`;
107
108
  }
@@ -0,0 +1 @@
1
+ export declare function executionLaneWordIsNone(provider: string): boolean;
@@ -0,0 +1,3 @@
1
+ export function executionLaneWordIsNone(provider) {
2
+ return provider === 'none' || provider === 'in-process';
3
+ }
@@ -51,6 +51,7 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
51
51
  "checkpoint.execution_outcome_unrecorded",
52
52
  "delegation.transcript_integrity",
53
53
  "delegation.ask_unresolvable",
54
+ "delegation.resume_lane_missing",
54
55
  "mcp.injection_dropped",
55
56
  "mcp.revocation_probe_failed",
56
57
  "mcp.server_redialed",
@@ -60,6 +61,8 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
60
61
  "workflow.agent_option_ignored",
61
62
  "memory.session_polluted",
62
63
  "memory.harvest_quarantined",
64
+ "memory.harvest_refused",
65
+ "memory.harvest_conflict",
63
66
  "memory.delegation_static_mark_waived",
64
67
  "memory.content_class_declared",
65
68
  "memory.hold_opened",
@@ -110,7 +113,6 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
110
113
  "fs.staging_file_swept",
111
114
  "session.lineage_rerooted",
112
115
  "instructions.source_changed",
113
- "session.records_skipped",
114
116
  "memory.layer_locked_legacy",
115
117
  "server.model_unreachable",
116
118
  "permission.entry_mode_clamped",
@@ -168,6 +170,7 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
168
170
  "checkpoint.execution_outcome_unrecorded": "operator",
169
171
  "delegation.transcript_integrity": "operator",
170
172
  "delegation.ask_unresolvable": "user",
173
+ "delegation.resume_lane_missing": "operator",
171
174
  "mcp.injection_dropped": "user",
172
175
  "mcp.revocation_probe_failed": "operator",
173
176
  "mcp.server_redialed": "operator",
@@ -177,6 +180,8 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
177
180
  "workflow.agent_option_ignored": "operator",
178
181
  "memory.session_polluted": "user",
179
182
  "memory.harvest_quarantined": "user",
183
+ "memory.harvest_refused": "user",
184
+ "memory.harvest_conflict": "user",
180
185
  "memory.delegation_static_mark_waived": "user",
181
186
  "memory.content_class_declared": "operator",
182
187
  "memory.hold_opened": "user",
@@ -227,7 +232,6 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
227
232
  "fs.staging_file_swept": "operator",
228
233
  "session.lineage_rerooted": "user",
229
234
  "instructions.source_changed": "user",
230
- "session.records_skipped": "user",
231
235
  "memory.layer_locked_legacy": "operator",
232
236
  "server.model_unreachable": "user",
233
237
  "permission.entry_mode_clamped": "user",
@@ -291,6 +295,7 @@ export const CORE_NOTICE_MINTER = Object.freeze({
291
295
  "checkpoint.execution_outcome_unrecorded": "engine",
292
296
  "delegation.transcript_integrity": "engine",
293
297
  "delegation.ask_unresolvable": "engine",
298
+ "delegation.resume_lane_missing": "engine",
294
299
  "mcp.injection_dropped": "engine",
295
300
  "mcp.revocation_probe_failed": "engine",
296
301
  "mcp.server_redialed": "engine",
@@ -300,6 +305,8 @@ export const CORE_NOTICE_MINTER = Object.freeze({
300
305
  "workflow.agent_option_ignored": "engine",
301
306
  "memory.session_polluted": "engine",
302
307
  "memory.harvest_quarantined": "engine",
308
+ "memory.harvest_refused": "engine",
309
+ "memory.harvest_conflict": "engine",
303
310
  "memory.delegation_static_mark_waived": "engine",
304
311
  "memory.content_class_declared": "engine",
305
312
  "memory.hold_opened": "engine",
@@ -350,7 +357,6 @@ export const CORE_NOTICE_MINTER = Object.freeze({
350
357
  "fs.staging_file_swept": "engine",
351
358
  "session.lineage_rerooted": "engine",
352
359
  "instructions.source_changed": "server",
353
- "session.records_skipped": "server",
354
360
  "memory.layer_locked_legacy": "server",
355
361
  "server.model_unreachable": "server",
356
362
  "permission.entry_mode_clamped": "server",
@@ -4,9 +4,10 @@ import type { AskAnsweredOutput } from './gateLedger.js';
4
4
  import type { ApprovalDenySettledBy, ToolApprovalRespondAckView, ToolApprovalRespondDecision, ToolApprovalRespondParkingView } from './toolApprovalWire.js';
5
5
  import { type DecisionRefusalReading, type RefusalRouteContext } from './decisionRefusal.js';
6
6
  import { type DeliveredReading, type LadderSafetyStop } from './attributionLadder.js';
7
+ import { type CheckpointRowVerdictReading } from '../checkpointRowVerdict.js';
7
8
  export declare const APPROVAL_RESOLUTION_LANES: readonly ["respond", "decide", "ask"];
8
9
  export type ApprovalResolutionLane = (typeof APPROVAL_RESOLUTION_LANES)[number];
9
- export declare const APPROVAL_NOT_SENT_CAUSES: readonly ["retracted", "edit_refused", "no_pending", "wrong_gate", "fetch_failed", "no_input", "card_unavailable", "safety_stop"];
10
+ export declare const APPROVAL_NOT_SENT_CAUSES: readonly ["retracted", "edit_refused", "no_pending", "wrong_gate", "fetch_failed", "no_input", "card_unavailable", "safety_stop", "row_unresumable"];
10
11
  export type ApprovalNotSentCause = (typeof APPROVAL_NOT_SENT_CAUSES)[number];
11
12
  export declare const APPROVAL_UNSETTLED_CAUSES: readonly ["engine_refused", "engine_error", "no_answer", "transport_exhausted", "parking_in_progress", "interrupted", "orchestration", "unreadable"];
12
13
  export type ApprovalUnsettledCause = (typeof APPROVAL_UNSETTLED_CAUSES)[number];
@@ -32,6 +33,9 @@ export interface ApprovalResolutionNotSent extends ApprovalResolutionBase {
32
33
  readonly kind: 'not_sent';
33
34
  readonly cause: ApprovalNotSentCause;
34
35
  readonly safetyCode?: Exclude<HitlSafetyCode, 'binding_mismatch' | 'no_pending' | 'wrong_gate'>;
36
+ readonly rowVerdict?: Exclude<CheckpointRowVerdictReading, {
37
+ readonly kind: 'ok';
38
+ }>;
35
39
  readonly detail?: string;
36
40
  }
37
41
  export interface ApprovalResolutionUnsettled extends ApprovalResolutionBase {
@@ -4,6 +4,8 @@ import { isSlotCarrier, thrownIsInstanceOf } from '../wireFailureShape.js';
4
4
  import { carrierReadOf, isLocalSafetyStop, localCodeReadOf, topCauseReadOf } from './attributionLadder.js';
5
5
  import { directFailureReadingOf, transportReadingOf } from './approvalDelivery.js';
6
6
  import { ownReadingSlot } from '../engineWireFor.js';
7
+ import { CHECKPOINT_ROW_BAR_SENTENCE } from '../checkpointRowVerdict.js';
8
+ import { msg, renderMessage } from '../i18n/translate.js';
7
9
  export const APPROVAL_RESOLUTION_LANES = Object.freeze(['respond', 'decide', 'ask']);
8
10
  export const APPROVAL_NOT_SENT_CAUSES = Object.freeze([
9
11
  'retracted',
@@ -14,6 +16,7 @@ export const APPROVAL_NOT_SENT_CAUSES = Object.freeze([
14
16
  'no_input',
15
17
  'card_unavailable',
16
18
  'safety_stop',
19
+ 'row_unresumable',
17
20
  ]);
18
21
  export const APPROVAL_UNSETTLED_CAUSES = Object.freeze([
19
22
  'engine_refused',
@@ -49,6 +52,7 @@ const NOT_SENT_SENTENCES = Object.freeze({
49
52
  no_input: 'the approval has no tool input or question to show, so no card was shown; nothing was sent',
50
53
  card_unavailable: 'no approval card could be shown on this host; nothing was sent',
51
54
  safety_stop: 'the decision was stopped by a local safety check before it was sent; nothing was sent',
55
+ row_unresumable: CHECKPOINT_ROW_BAR_SENTENCE,
52
56
  });
53
57
  function nonEmpty(v) {
54
58
  return typeof v === 'string' && v.length > 0 ? v : undefined;
@@ -77,8 +81,21 @@ export function approvalResolutionDetail(r) {
77
81
  if (cause === undefined || !Object.hasOwn(NOT_SENT_SENTENCES, cause))
78
82
  return undefined;
79
83
  const sentence = NOT_SENT_SENTENCES[cause];
84
+ if (cause === 'row_unresumable') {
85
+ let verdict;
86
+ try {
87
+ const d = Object.getOwnPropertyDescriptor(r, 'rowVerdict');
88
+ verdict = d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
89
+ }
90
+ catch {
91
+ verdict = undefined;
92
+ }
93
+ return ownStr(verdict, 'kind') !== 'unreadable' ? (ownStr(verdict, 'sentence') ?? sentence) : sentence;
94
+ }
80
95
  const code = cause === 'safety_stop' ? ownStr(r, 'safetyCode') : undefined;
81
- return code !== undefined ? `${sentence} (${code})` : sentence;
96
+ if (code === undefined)
97
+ return sentence;
98
+ return code === 'binding_missing' ? `${sentence} (${code}) · ${renderMessage(msg('approval.decide.unboundWayOut'))}` : `${sentence} (${code})`;
82
99
  }
83
100
  if (kind !== 'unsettled')
84
101
  return undefined;
@@ -3,6 +3,7 @@ import { unrefTimer } from '../unrefTimer.js';
3
3
  import { readLivePendingRows } from './livePendingAsk.js';
4
4
  import { createStreamDrainingLegs } from '../streamDrainingLegs.js';
5
5
  import { approvalQueueRowFamilyOf, peerInboundQueueRowOf } from './approvalQueueRow.js';
6
+ import { parkRowDelegationLinksOf } from './parkRowDelegation.js';
6
7
  import { checkpointRowVerdictOf } from '../checkpointRowVerdict.js';
7
8
  const _realClientFitsFeedSlice = true;
8
9
  void _realClientFitsFeedSlice;
@@ -49,7 +50,7 @@ function readDurablePendingRows(raw) {
49
50
  }
50
51
  function durableDigestOf(rows) {
51
52
  return rows
52
- .map(r => [r.sessionId ?? '', r.taskId ?? '', r.toolCallId ?? '', r.boundCallId ?? '', r.boundInputHash ?? '', r.toolName ?? '', JSON.stringify(checkpointRowVerdictOf(r) ?? null)]
53
+ .map(r => [r.sessionId ?? '', r.taskId ?? '', r.toolCallId ?? '', r.boundCallId ?? '', r.boundInputHash ?? '', r.toolName ?? '', JSON.stringify(checkpointRowVerdictOf(r) ?? null), JSON.stringify(parkRowDelegationLinksOf(r) ?? null)]
53
54
  .join(''))
54
55
  .sort()
55
56
  .join('');
@@ -3,6 +3,8 @@ import { isAskTool, questionCardVerdict } from './frameRouter.js';
3
3
  import { approvalCallKey } from './gateIdentity.js';
4
4
  import { askQuestionsFromPending, pendingRowIsOwnedByThisSession } from './parkOwnership.js';
5
5
  import { approvalQueueRowFamilyOf, checkpointQueueRowSortOf, QUEUE_ROW_FAMILY_UNREADABLE_GATE } from './approvalQueueRow.js';
6
+ import { checkpointRowBarOf, checkpointRowBarSentence } from '../checkpointRowVerdict.js';
7
+ import { msg, renderMessage } from '../i18n/translate.js';
6
8
  export const ASK_PARK_ROW_WAIT_MS = 15000;
7
9
  export const ASK_PARK_ROW_POLL_MS = 500;
8
10
  export function classifyAskParkRows(queueRows, taskId, deps) {
@@ -29,6 +31,14 @@ export function classifyAskParkRows(queueRows, taskId, deps) {
29
31
  };
30
32
  }
31
33
  const foreignKind = familyUnreadable.has(pending) ? QUEUE_ROW_FAMILY_UNREADABLE_GATE : askParkForeignGateKind(pending);
34
+ const bar = foreignKind === null || foreignKind === QUEUE_ROW_FAMILY_UNREADABLE_GATE ? checkpointRowBarOf(pending) : undefined;
35
+ if (bar !== undefined) {
36
+ return {
37
+ kind: 'settled',
38
+ reason: renderMessage(msg('approval.rowUnresumable.askNotSurfaced', { taskId: String(pending.taskId), sentence: checkpointRowBarSentence(bar) })),
39
+ rowVerdict: bar,
40
+ };
41
+ }
32
42
  if (foreignKind !== null) {
33
43
  return {
34
44
  kind: 'unborn',
@@ -129,6 +139,8 @@ export function pendingRowIsOwnedAndDecidable(row, deps) {
129
139
  return true;
130
140
  if (leg !== 'approval')
131
141
  return false;
142
+ if (checkpointRowBarOf(row) !== undefined)
143
+ return false;
132
144
  return askParkRowArm(row) === 'question' ? askQuestionsFromPending(row) !== null : true;
133
145
  }
134
146
  export function countOwnedPendingRows(listed, deps) {
@@ -20,6 +20,7 @@ export interface HitlApprovalDecisionRequest {
20
20
  readonly taskId?: string;
21
21
  readonly toolCallId?: string;
22
22
  readonly answer: ApprovalCardDecision;
23
+ readonly card?: object;
23
24
  }
24
25
  export interface HitlAskDecisionRequest {
25
26
  readonly family: 'ask';
@@ -28,6 +29,7 @@ export interface HitlAskDecisionRequest {
28
29
  readonly approvalId?: string;
29
30
  readonly toolCallId?: string;
30
31
  readonly answer: ApprovalCardDecision;
32
+ readonly card?: object;
31
33
  }
32
34
  export interface HitlElicitationDecisionRequest {
33
35
  readonly family: 'elicitation';
@@ -10,7 +10,7 @@ import { readDecisionNoteAudit } from './approvalDecisionNoteAudit.js';
10
10
  import { carrierReadOf, deliverWithAttributionLadder, isLocalSafetyStop } from './attributionLadder.js';
11
11
  import { budgetSpent, deliverApprovalDecision, deliveryTiming, isAborted, newSendTally, PARKING_WAIT_CAP_MS, setDeliveryTimingForTests, sleepFor, settledTransportReadingOf, withTransportRetry, deliveryReadingOf, } from './approvalDelivery.js';
12
12
  import { ASK_DECIDED_CODE, classifyDeliveredDecisionRefusal, decisionRefusalClosesOut, notPendingAfterRetry } from './decisionRefusal.js';
13
- import { sessionAllowCarried } from './sessionAllowRoute.js';
13
+ import { sessionAllowEligible } from './sessionAllowRoute.js';
14
14
  import { addDecisionOpKeys, closeDecisionOp, decisionLedgerKeysOf, decisionOpsWithKey, openDecisionOp } from './decisionLedger.js';
15
15
  import { hostHoldIsActive, takeHostHoldLedgerOp, tryHoldApprovalNow } from './approvalHolds.js';
16
16
  import { answerPayloadFor, decisionAttributionKeyOf, isToolApprovalRespondReplayed, readApprovalDenySettledBy, readToolApprovalRespondAck, readToolApprovalRespondParking, } from './toolApprovalWire.js';
@@ -195,7 +195,7 @@ async function sendAskFamily(respondForFallback, decideAsk, req, ctx) {
195
195
  if (payload === undefined)
196
196
  return { family: 'ask', attempts: ctx.tally.attempts, kind: 'not_sent', cause: 'not_an_answer' };
197
197
  let fallbackRespond = respondForFallback;
198
- if (req.answer.kind === 'allow' && req.answer.allowSession === true && respondForFallback !== undefined && req.approvalId !== undefined && sessionAllowCarried('v1', req.approvalId)) {
198
+ if (req.answer.kind === 'allow' && req.answer.allowSession === true && respondForFallback !== undefined && req.approvalId !== undefined) {
199
199
  const viaAnswerGate = await sendApprovalFamily(respondForFallback, req.approvalId, req.answer, ctx);
200
200
  if (!answerGateCannotTake(viaAnswerGate))
201
201
  return { ...viaAnswerGate, family: 'ask', allowSessionCarried: true };
@@ -386,6 +386,14 @@ function snapshotAnswer(raw) {
386
386
  }
387
387
  return typeof kind === 'string' ? Object.freeze({ kind }) : undefined;
388
388
  }
389
+ function sessionGatedAnswer(answer, card) {
390
+ if (answer.kind !== 'allow' || answer.allowSession !== true)
391
+ return { answer };
392
+ const why = card === undefined ? 'card_absent' : typeof card === 'object' && card !== null ? sessionAllowEligible(card) : 'card_unreadable';
393
+ if (why === 'given')
394
+ return { answer };
395
+ return { answer: Object.freeze({ ...answer, allowSession: false }), dropped: why };
396
+ }
389
397
  function readRequest(req) {
390
398
  if (typeof req !== 'object' || req === null)
391
399
  return undefined;
@@ -399,17 +407,19 @@ function readRequest(req) {
399
407
  const rawAnswer = guardedRead(() => readOwnDataValue(req, 'answer'));
400
408
  if (typeof rawAnswer !== 'object' || rawAnswer === null)
401
409
  return undefined;
402
- const answer = snapshotAnswer(rawAnswer);
403
- if (answer === undefined)
410
+ const snapshot = snapshotAnswer(rawAnswer);
411
+ if (snapshot === undefined)
404
412
  return undefined;
413
+ const { answer, dropped } = sessionGatedAnswer(snapshot, guardedRead(() => readOwnDataValue(req, 'card')));
414
+ const droppedFact = dropped !== undefined ? { sessionAllowDropped: dropped } : {};
405
415
  if (family === 'approval') {
406
416
  const approvalId = s('approvalId');
407
- return approvalId !== undefined ? { family, approvalId, taskId: s('taskId'), toolCallId: s('toolCallId'), answer } : undefined;
417
+ return approvalId !== undefined ? { family, approvalId, taskId: s('taskId'), toolCallId: s('toolCallId'), answer, ...droppedFact } : undefined;
408
418
  }
409
419
  if (family === 'ask') {
410
420
  const taskId = s('taskId');
411
421
  const askId = s('askId');
412
- return taskId !== undefined && askId !== undefined ? { family, taskId, askId, approvalId: s('approvalId'), toolCallId: s('toolCallId'), answer } : undefined;
422
+ return taskId !== undefined && askId !== undefined ? { family, taskId, askId, approvalId: s('approvalId'), toolCallId: s('toolCallId'), answer, ...droppedFact } : undefined;
413
423
  }
414
424
  return undefined;
415
425
  }
@@ -519,6 +529,10 @@ export async function sendHitlDecision(request, deps) {
519
529
  out = outcomeOfThrown(e, 'elicitation', undefined, signal, ctx.tally, false);
520
530
  }
521
531
  }
532
+ if (req.family !== 'elicitation' && req.sessionAllowDropped !== undefined) {
533
+ hostLog('debug', `decisionSend: session-allow-not-eligible ${req.family}=${req.family === 'ask' ? req.askId : req.approvalId} why=${req.sessionAllowDropped} via=allow-once`);
534
+ out = { ...out, rememberDropped: true };
535
+ }
522
536
  const ambiguous = out.kind === 'unconfirmed' || (out.kind === 'not_applied' && (out.cause === 'parking' || out.cause === 'refused' || out.cause === 'idempotency_crossed'));
523
537
  if (ambiguous && out.cause !== 'aborted') {
524
538
  const gate = req.family === 'elicitation'
@@ -3,6 +3,7 @@ import type { PermissionResult } from '@sema-agent/agent-types';
3
3
  import { type CapturedLocalCode, type CauseSlotRead } from '../engineWireFor.js';
4
4
  import type { ApprovalsListEnvelope } from './crashConverged.js';
5
5
  import { type SuspendedReopenReading } from './suspendedReopen.js';
6
+ import type { CheckpointRowVerdictReading } from '../checkpointRowVerdict.js';
6
7
  export declare const DEFAULT_DENY_REASON = "The user rejected this tool use";
7
8
  export declare const MAX_DENY_REASON_CHARS = 4096;
8
9
  export declare function denyReasonForWire(reason: string | undefined, tag: string): string | undefined;
@@ -132,7 +133,10 @@ export type FindPendingOutcome = {
132
133
  } | {
133
134
  ok: false;
134
135
  reason: string;
135
- code?: 'no_pending' | 'wrong_gate';
136
+ code?: 'no_pending' | 'wrong_gate' | 'row_unresumable';
137
+ rowVerdict?: Exclude<CheckpointRowVerdictReading, {
138
+ readonly kind: 'ok';
139
+ }>;
136
140
  };
137
141
  export declare function findPendingForTask(client: HitlClientLike, taskId: string, matches: (toolName: string | undefined) => boolean, opts?: {
138
142
  signal?: AbortSignal;
@@ -11,6 +11,8 @@ import { suspendedReopenOf } from './suspendedReopen.js';
11
11
  import { closeDecisionOp, decisionLedgerKeysOf, openDecisionOp } from './decisionLedger.js';
12
12
  import { askParkBlockGeneration, noteAskParkDecideSettled } from './askParkAutoReopenBlock.js';
13
13
  import { checkpointQueueRowSortOf, QUEUE_ROW_FAMILY_UNREADABLE_GATE } from './approvalQueueRow.js';
14
+ import { checkpointRowBarOf, checkpointRowBarSentence } from '../checkpointRowVerdict.js';
15
+ import { msg, renderMessage } from '../i18n/translate.js';
14
16
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
15
17
  export const MAX_DENY_REASON_CHARS = 4096;
16
18
  export function denyReasonForWire(reason, tag) {
@@ -283,6 +285,9 @@ export async function findPendingForTask(client, taskId, matches, opts, callId,
283
285
  ? readings.find((x) => x.r.taskId === taskId && (x.r.boundCallId === callId || x.r.toolCallId === callId) && x.foreign !== null)
284
286
  : undefined;
285
287
  if (identityForeign !== undefined) {
288
+ const identityBar = identityForeign.foreign === QUEUE_ROW_FAMILY_UNREADABLE_GATE ? checkpointRowBarOf(identityForeign.r) : undefined;
289
+ if (identityBar !== undefined)
290
+ return { ok: false, reason: checkpointRowBarSentence(identityBar), code: 'row_unresumable', rowVerdict: identityBar };
286
291
  return {
287
292
  ok: false,
288
293
  reason: `the pending row this park names (call ${callId}) is parked on a '${String(identityForeign.foreign)}' gate — not an approval this leg decides; refusing to decide a different row`,
@@ -294,8 +299,19 @@ export async function findPendingForTask(client, taskId, matches, opts, callId,
294
299
  const pending = (identityConflicts ? undefined : identityRow) ??
295
300
  pool.find((r) => r.taskId === taskId && matches(typeof r.toolName === 'string' ? r.toolName : undefined)) ??
296
301
  pool.find((r) => r.taskId === taskId);
297
- if (!pending)
302
+ if (!pending) {
303
+ for (const x of readings) {
304
+ if (x.r.taskId !== taskId || x.foreign !== QUEUE_ROW_FAMILY_UNREADABLE_GATE)
305
+ continue;
306
+ const unreadableBar = checkpointRowBarOf(x.r);
307
+ if (unreadableBar !== undefined)
308
+ return { ok: false, reason: checkpointRowBarSentence(unreadableBar), code: 'row_unresumable', rowVerdict: unreadableBar };
309
+ }
298
310
  return { ok: false, reason: 'no pending checkpoint for this run (resolved/expired?)', code: 'no_pending' };
311
+ }
312
+ const bar = checkpointRowBarOf(pending);
313
+ if (bar !== undefined)
314
+ return { ok: false, reason: checkpointRowBarSentence(bar), code: 'row_unresumable', rowVerdict: bar };
299
315
  return { ok: true, pending, gatedCallId: pending.toolCallId ?? pending.boundCallId ?? undefined };
300
316
  }
301
317
  export class HitlBridge {
@@ -349,7 +365,7 @@ export class HitlBridge {
349
365
  if (typeof boundCallId !== 'string' || boundCallId.length === 0 || typeof boundInputHash !== 'string' || boundInputHash.length === 0) {
350
366
  throw new HitlSafetyError(`pending checkpoint ${p.id ?? '<no-id>'} is missing D-1 binding fields ` +
351
367
  `(boundCallId=${String(boundCallId)}, boundInputHash=${String(boundInputHash)}) — ` +
352
- 'pre-D-1 workers are no longer supported (server 5.x requires the verbatim two-tuple echo)', 'binding_missing');
368
+ `the row itself carries no binding to echo, so ${renderMessage(msg('approval.decide.unboundWayOut'))}`, 'binding_missing');
353
369
  }
354
370
  return { boundCallId, boundInputHash };
355
371
  }
@@ -358,6 +374,8 @@ export class HitlBridge {
358
374
  if (!pending) {
359
375
  throw new HitlSafetyError('no pending checkpoint to decide (resolved/expired under the human) — refetch + re-present', 'no_pending');
360
376
  }
377
+ if (preResolvedPending !== undefined)
378
+ assertPendingRowResumable(preResolvedPending);
361
379
  const decision = outcome.decision === 'approve'
362
380
  ? {
363
381
  decision: 'approve',
@@ -383,6 +401,8 @@ export class HitlBridge {
383
401
  if (!pending) {
384
402
  throw new HitlSafetyError('no pending AskUserQuestion checkpoint to answer — refetch + re-present', 'no_pending');
385
403
  }
404
+ if (preResolvedPending !== undefined)
405
+ assertPendingRowResumable(preResolvedPending);
386
406
  const decision = {
387
407
  decision: 'approve',
388
408
  answer: { answers: answers.map((a) => marshalAnswer(a)) },
@@ -541,6 +561,11 @@ export function makeHitlCanUseTool(bridge, prompt) {
541
561
  return decision;
542
562
  };
543
563
  }
564
+ function assertPendingRowResumable(pending) {
565
+ const bar = checkpointRowBarOf(pending);
566
+ if (bar !== undefined)
567
+ throw new Error(checkpointRowBarSentence(bar));
568
+ }
544
569
  function assertAnswersSendable(answers) {
545
570
  if (!Array.isArray(answers) || answers.length === 0) {
546
571
  throw new HitlSafetyError('refusing to send an EMPTY AskUserQuestion answer set — `{answers:[]}` is the wire\'s DENY/NO_HUMAN form, ' +
@@ -1,6 +1,7 @@
1
1
  import { isApprovalRequestFrameV1 } from '@sema-agent/sdk';
2
2
  import { appendOwnElement } from '../ownKey.js';
3
3
  import { readRequiresRealApproval } from './approvalCardReading.js';
4
+ import { pendingRowIsOfSession } from './parkRowDelegation.js';
4
5
  import { approvalLiveFaceOf, liveFaceCarried } from './approvalLiveFace.js';
5
6
  import { THROWN_SLOT_UNREADABLE, readThrownSlot } from '../wireFailureShape.js';
6
7
  import { isToolApprovalFrame, surfaceToolApprovalFrameAndRespond, } from './toolApprovalWire.js';
@@ -124,7 +125,8 @@ export function countApprovalsAwaitingDecision(snapshot, filter) {
124
125
  if (snapshot.kind === 'unknown')
125
126
  return { durable: null, live: null, suspendedSubagent: null };
126
127
  const bySession = (rows) => scope.kind === 'session' ? rows.filter(r => r.sessionId === scope.sessionId) : [...rows];
127
- const durable = (snapshot.pendingDropped ?? 0) > 0 ? null : Array.isArray(snapshot.pending) ? bySession(snapshot.pending).length : null;
128
+ const durableOfSession = (rows) => scope.kind === 'session' ? rows.filter(r => pendingRowIsOfSession(r, scope.sessionId)).length : rows.length;
129
+ const durable = (snapshot.pendingDropped ?? 0) > 0 ? null : Array.isArray(snapshot.pending) ? durableOfSession(snapshot.pending) : null;
128
130
  if (!Array.isArray(snapshot.livePending))
129
131
  return { durable, live: null, suspendedSubagent: null };
130
132
  if ((snapshot.livePendingDropped ?? 0) > 0)
@@ -138,7 +140,7 @@ export function livePendingRowToApprovalFrame(row) {
138
140
  approvalId: row.approvalId,
139
141
  toolName: row.toolName,
140
142
  ...(row.fromSubagent === true ? { fromSubagent: true } : {}),
141
- ...(row.originTaskId !== undefined ? { sourceTaskId: row.originTaskId } : {}),
143
+ ...(row.originTaskId !== undefined ? { sourceSessionId: row.originTaskId } : {}),
142
144
  ...(row.governanceForced === true ? { governanceForced: true } : {}),
143
145
  ...(readRequiresRealApproval(row) ? { requiresRealApproval: true } : {}),
144
146
  ...liveFaceCarried(approvalLiveFaceOf(row)),