@sema-agent/client-core 0.93.0 → 0.94.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.
- package/CHANGELOG.md +261 -0
- package/README.md +31 -18
- package/dist/adapt/arms.js +8 -5
- package/dist/adapter/downstream/terminalToSdkResult.js +2 -3
- package/dist/agentSession/engineSessionBackgroundStop.js +2 -1
- package/dist/agentsWireCaps.d.ts +15 -0
- package/dist/agentsWireCaps.js +111 -1
- package/dist/approvalReachWire.d.ts +18 -0
- package/dist/approvalReachWire.js +58 -0
- package/dist/approvalsStreamLiveCapability.js +16 -11
- package/dist/callerCwdCapability.d.ts +16 -0
- package/dist/callerCwdCapability.js +47 -0
- package/dist/controlRouter.js +3 -10
- package/dist/coreValuePorts.d.ts +64 -1
- package/dist/coreValuePorts.js +30 -0
- package/dist/detachWire.d.ts +1 -4
- package/dist/detachWire.js +1 -1
- package/dist/deviceExecutorManagementCapability.js +39 -34
- package/dist/deviceFlowPolling.d.ts +5 -0
- package/dist/deviceFlowPolling.js +28 -0
- package/dist/displayUntrusted.js +652 -21
- package/dist/engineErrorCodes.d.ts +5 -1
- package/dist/engineErrorCodes.js +9 -12
- package/dist/engineNoticeCodes.d.ts +22 -2
- package/dist/engineNoticeCodes.js +73 -6
- package/dist/engineRefusedToolName.d.ts +1 -0
- package/dist/engineRefusedToolName.js +8 -0
- package/dist/executionLaneCapability.js +14 -9
- package/dist/fileHistoryCaptureCapability.js +14 -9
- package/dist/fileHistoryLegNotice.d.ts +11 -0
- package/dist/fileHistoryLegNotice.js +50 -13
- package/dist/fleet/fleetLedger.js +5 -0
- package/dist/gateOutcome.d.ts +2 -1
- package/dist/gateOutcome.js +3 -0
- package/dist/gateVocabulary.js +1 -1
- package/dist/generated/engineAskFaceTables.d.ts +3 -0
- package/dist/generated/engineAskFaceTables.js +12 -0
- package/dist/generated/engineFactTables.d.ts +4 -2
- package/dist/generated/engineFactTables.js +13 -0
- package/dist/generated/engineNoticeTables.d.ts +5 -0
- package/dist/generated/engineNoticeTables.js +7 -0
- package/dist/generated/sdkFactTables.d.ts +3 -0
- package/dist/generated/sdkFactTables.js +17 -0
- package/dist/generated/settingsSchemaTables.d.ts +3 -0
- package/dist/generated/settingsSchemaTables.js +3 -0
- package/dist/headlessPermissionModeWire.d.ts +2 -2
- package/dist/headlessPermissionModeWire.js +11 -21
- package/dist/hitl/approvalHolds.d.ts +36 -0
- package/dist/hitl/approvalHolds.js +89 -0
- package/dist/hitl/approvalOutcomeNote.js +2 -1
- package/dist/hitl/approvalResolution.d.ts +3 -2
- package/dist/hitl/approvalResolution.js +21 -0
- package/dist/hitl/approvalsFeed.js +5 -0
- package/dist/hitl/askGateWire.js +3 -0
- package/dist/hitl/automaticAnswerVerdict.d.ts +8 -2
- package/dist/hitl/automaticAnswerVerdict.js +69 -21
- package/dist/hitl/decisionRefusal.d.ts +19 -0
- package/dist/hitl/decisionRefusal.js +63 -0
- package/dist/hitl/frameRouter.d.ts +10 -0
- package/dist/hitl/frameRouter.js +59 -3
- package/dist/hitl/gateLedger.d.ts +3 -0
- package/dist/hitl/gateLedger.js +17 -0
- package/dist/hitl/hitlBridge.d.ts +2 -2
- package/dist/hitl/hitlBridge.js +8 -10
- package/dist/hitl/hitlHostSurface.d.ts +6 -0
- package/dist/hitl/hitlHostSurface.js +25 -4
- package/dist/hitl/localAllowRule.js +2 -1
- package/dist/hitl/noAllowCause.d.ts +9 -0
- package/dist/hitl/noAllowCause.js +26 -0
- package/dist/hitl/parkResolver.js +6 -3
- package/dist/hitl/parkRowBirthWait.js +2 -1
- package/dist/hitl/persistedRulesWire.d.ts +6 -1
- package/dist/hitl/persistedRulesWire.js +12 -4
- package/dist/hitl/planReviewWire.d.ts +0 -1
- package/dist/hitl/planReviewWire.js +30 -9
- package/dist/hitl/sessionPolicyDeliverable.d.ts +1 -1
- package/dist/hitl/sessionPolicyDeliverable.js +3 -8
- package/dist/hitl/sessionPolicyWire.js +1 -1
- package/dist/hitl/toolApprovalWire.d.ts +37 -2
- package/dist/hitl/toolApprovalWire.js +392 -13
- package/dist/humanTurnRead.d.ts +15 -1
- package/dist/humanTurnRead.js +28 -2
- package/dist/index.d.ts +18 -6
- package/dist/index.js +14 -5
- package/dist/limitsWire.d.ts +0 -1
- package/dist/limitsWire.js +2 -10
- package/dist/liveModelCatalog.d.ts +14 -0
- package/dist/liveModelCatalog.js +61 -0
- package/dist/mcpProbeCapability.js +14 -9
- package/dist/mcpReconnect.js +13 -8
- package/dist/memoryComplianceCapability.js +15 -10
- package/dist/memoryOriginCapability.js +15 -10
- package/dist/modelIdentityResolvability.d.ts +4 -2
- package/dist/modelIdentityResolvability.js +31 -4
- package/dist/outputStyleSeatCapability.d.ts +16 -0
- package/dist/outputStyleSeatCapability.js +47 -0
- package/dist/ownKey.d.ts +1 -0
- package/dist/ownKey.js +10 -0
- package/dist/peerLaneCapability.js +16 -11
- package/dist/permissionRuleIssue.js +1 -1
- package/dist/permissionRulesWriteCapability.js +16 -11
- package/dist/permissionsAllowGrantCapability.d.ts +2 -0
- package/dist/permissionsAllowGrantCapability.js +38 -16
- package/dist/permissionsDeclaration.d.ts +10 -1
- package/dist/permissionsDeclaration.js +97 -15
- package/dist/protocolSegment.d.ts +1 -0
- package/dist/protocolSegment.js +13 -1
- package/dist/request/taskRequest.d.ts +11 -3
- package/dist/request/taskRequest.js +13 -1
- package/dist/resultFrameKeys.d.ts +4 -0
- package/dist/resultFrameKeys.js +13 -0
- package/dist/resumeRefusalCopy.js +6 -16
- package/dist/retryStatus.js +6 -4
- package/dist/rewindArchiveCapability.js +13 -8
- package/dist/{hitl/rosterMountedNames.d.ts → rosterMountedNames.d.ts} +1 -1
- package/dist/runCancelContext.js +2 -1
- package/dist/sealedKeyCapability.js +19 -14
- package/dist/seatContract.d.ts +33 -5
- package/dist/seatContract.js +143 -2
- package/dist/sessionPolicyCapability.js +15 -10
- package/dist/sessionReadRedactionCapability.d.ts +16 -0
- package/dist/sessionReadRedactionCapability.js +47 -0
- package/dist/sessionSyncFileHistoryCapability.js +14 -9
- package/dist/skillToolListsCapability.js +14 -9
- package/dist/sqlEngineCapability.js +18 -13
- package/dist/steerPriorityCapability.js +21 -16
- package/dist/subagent/agentStateDetail.d.ts +1 -0
- package/dist/subagent/agentStateDetail.js +41 -0
- package/dist/subagent/engineCompactWire.d.ts +15 -0
- package/dist/subagent/engineCompactWire.js +75 -13
- package/dist/supportFloor.js +1 -1
- package/dist/taskRequestWords.d.ts +2 -4
- package/dist/taskRequestWords.js +3 -2
- package/dist/types/engineState.d.ts +1 -1
- package/dist/userAttachmentEnvelope.d.ts +22 -1
- package/dist/userAttachmentEnvelope.js +90 -2
- package/dist/userInvokedSkillsCapability.js +14 -9
- package/dist/webSearchBackendCapability.js +21 -16
- package/dist/wireErrorTriage.d.ts +11 -0
- package/dist/wireErrorTriage.js +35 -0
- package/dist/wireFailureShape.d.ts +6 -0
- package/dist/wireFailureShape.js +30 -3
- package/dist/wireRefusalCopy.d.ts +6 -1
- package/dist/wireRefusalCopy.js +22 -8
- package/dist/wiringInstructionsAttribution.d.ts +10 -0
- package/dist/wiringInstructionsAttribution.js +67 -0
- package/dist/workflowClient.js +22 -1
- package/dist/workflowsJournalCapability.js +14 -9
- package/dist/writeProtectionCapability.js +28 -23
- package/docs/INTEGRATION-CLIENTS.md +817 -45
- package/package.json +5 -5
- package/dist/askSurvivesStandingAnswer.d.ts +0 -13
- package/dist/askSurvivesStandingAnswer.js +0 -53
- package/dist/hitl/askSurvivesPosture.d.ts +0 -20
- package/dist/hitl/askSurvivesPosture.js +0 -48
- /package/dist/{hitl/rosterMountedNames.js → rosterMountedNames.js} +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { refusalSaysStillPending } from '../engineErrorCodes.js';
|
|
2
|
+
import { readLivePendingRows } from './livePendingAsk.js';
|
|
3
|
+
import { readToolApprovalRespondRefusal } from './toolApprovalWire.js';
|
|
4
|
+
const ASK_DECIDED_CODE = 'conflict.ask_decided';
|
|
5
|
+
const TRANSIENT_STATUSES = new Set([408, 425, 429, 502, 503, 504]);
|
|
6
|
+
const DISPLAYABLE_CODE = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
7
|
+
export function classifyDecisionRefusal(err) {
|
|
8
|
+
const r = readToolApprovalRespondRefusal(err);
|
|
9
|
+
const status = r?.status;
|
|
10
|
+
if (status === undefined || !Number.isInteger(status) || status < 400 || status > 599)
|
|
11
|
+
return undefined;
|
|
12
|
+
const code = r?.errorCode;
|
|
13
|
+
const errorCode = code !== undefined && DISPLAYABLE_CODE.test(code) ? code : undefined;
|
|
14
|
+
const standing = status === 404 || status === 410 || (status === 409 && code === ASK_DECIDED_CODE)
|
|
15
|
+
? 'settled-elsewhere'
|
|
16
|
+
: status >= 500 && status !== 501
|
|
17
|
+
? 'ambiguous'
|
|
18
|
+
: 'refused-pending';
|
|
19
|
+
const transient = TRANSIENT_STATUSES.has(status) || (typeof err === 'object' && err !== null && refusalSaysStillPending(err));
|
|
20
|
+
return Object.freeze({ standing, status, ...(errorCode !== undefined ? { errorCode } : {}), ...(transient ? { transient: true } : {}) });
|
|
21
|
+
}
|
|
22
|
+
export function liveAskPendingReading(envelope, approvalId) {
|
|
23
|
+
if (typeof approvalId !== 'string' || approvalId === '')
|
|
24
|
+
return 'unknown';
|
|
25
|
+
try {
|
|
26
|
+
const reading = readLivePendingRows(envelope);
|
|
27
|
+
if (reading.kind !== 'present')
|
|
28
|
+
return 'unknown';
|
|
29
|
+
if (reading.rows.some((row) => row.approvalId === approvalId))
|
|
30
|
+
return 'pending';
|
|
31
|
+
return reading.dropped === 0 ? 'not_pending' : 'unknown';
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function decisionRefusalClosesOut(refusal, pending) {
|
|
38
|
+
if (pending !== 'pending' || typeof refusal !== 'object' || refusal === null)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
const standing = refusal.standing;
|
|
42
|
+
if (refusal.transient === true)
|
|
43
|
+
return false;
|
|
44
|
+
return standing === 'ambiguous' || standing === 'refused-pending';
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function undeliverableDecisionContent(facts) {
|
|
51
|
+
const code = typeof facts.errorCode === 'string' && DISPLAYABLE_CODE.test(facts.errorCode) ? ` ${facts.errorCode}` : '';
|
|
52
|
+
const head = `The engine refused the approval decision this client sent (HTTP ${String(facts.status)}${code}), so the call it asked about could not be answered from here and did not run`;
|
|
53
|
+
const runId = typeof facts.runId === 'string' && DISPLAYABLE_CODE.test(facts.runId) ? ` ${facts.runId}` : '';
|
|
54
|
+
switch (facts.end) {
|
|
55
|
+
case 'run-stopped':
|
|
56
|
+
return `${head}; the run was stopped instead of being left waiting for the approval window to close. Update the client, or run the task in an interactive session and answer the approval there.`;
|
|
57
|
+
case 'run-unconfirmed':
|
|
58
|
+
return `${head}; this client stopped waiting, but the engine did not confirm that it stopped the run${runId}, so that approval may stay open until its window closes. Update the client, or run the task in an interactive session and answer the approval there.`;
|
|
59
|
+
case 'turn-stopped':
|
|
60
|
+
default:
|
|
61
|
+
return `${head}; this turn was stopped instead of being left waiting. Update the client, or switch to a permission mode that shows you the approval and try again.`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -2,9 +2,19 @@ import type { AgentEvent } from '@sema-agent/sdk';
|
|
|
2
2
|
import type { DecideOperationObserver, HitlClientLike } from './hitlBridge.js';
|
|
3
3
|
import { type RespondToolApprovalFn, type ToolApprovalFrame, type ToolApprovalFrameLaneOpts, type ToolApprovalFrameOutcome } from './toolApprovalWire.js';
|
|
4
4
|
import type { GateLedger } from './gateLedger.js';
|
|
5
|
+
import type { NoAllowCause } from './noAllowCause.js';
|
|
5
6
|
import { type ReplayedApprovalDisposition } from './replayedApproval.js';
|
|
6
7
|
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.";
|
|
7
8
|
export declare const HITL_POLICY_DENY_MESSAGE: string;
|
|
9
|
+
export type PolicyDenyLane = 'no_approval_ui' | 'dont_ask';
|
|
10
|
+
export interface PolicyDenyReasonFacts {
|
|
11
|
+
readonly lane: PolicyDenyLane;
|
|
12
|
+
readonly toolName: string;
|
|
13
|
+
readonly cause?: NoAllowCause;
|
|
14
|
+
readonly allowRuleFlag?: string;
|
|
15
|
+
readonly budgetSetting?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function policyDenyReason(facts: PolicyDenyReasonFacts): string;
|
|
8
18
|
export declare const HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
|
|
9
19
|
export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
10
20
|
export declare function toolEndOutputText(output: unknown): string | undefined;
|
package/dist/hitl/frameRouter.js
CHANGED
|
@@ -4,6 +4,8 @@ import { classifierDenyFromToolEnd } from '../classifierVerdictWire.js';
|
|
|
4
4
|
import { GATE_PARKED_ERROR_CODE } from '../engineErrorCodes.js';
|
|
5
5
|
import { readRunTerminal, runTerminalCode, runTerminalGateKind, runTerminalGateToolName } from '../runTerminal.js';
|
|
6
6
|
import { surfaceForCurrentSession, surfaceReplayedApprovalListUnreadable } from './hitlHostSurface.js';
|
|
7
|
+
import { liveCardHoldOf, supersedeLiveCard } from './approvalHolds.js';
|
|
8
|
+
import { liveFrameCallKey } from './gateIdentity.js';
|
|
7
9
|
import { SEMA_DENIAL_KIND_KEY, hasLedgerPositionKey } from './gateLedger.js';
|
|
8
10
|
import { replayedApprovalDisposition } from './replayedApproval.js';
|
|
9
11
|
import { settleWithinDeadline } from '../abortableSleep.js';
|
|
@@ -14,6 +16,38 @@ export const HITL_REJECT_MESSAGE = REJECT_MESSAGE;
|
|
|
14
16
|
export const HITL_POLICY_DENY_MESSAGE = 'Permission for this tool use was denied: it requires interactive approval, and permission prompts are ' +
|
|
15
17
|
'not available in this session. The action was NOT performed. Do not claim it succeeded, and do not retry ' +
|
|
16
18
|
'it in this session \u2014 report the limitation to the user, or suggest an alternative.';
|
|
19
|
+
const SENTENCE_TOOL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.:@-]{0,199}$/;
|
|
20
|
+
export function policyDenyReason(facts) {
|
|
21
|
+
const one = typeof facts.toolName === 'string' && SENTENCE_TOOL_NAME.test(facts.toolName) ? `this one "${facts.toolName}" call` : 'this one call';
|
|
22
|
+
const flag = typeof facts.allowRuleFlag === 'string' && SENTENCE_TOOL_NAME.test(facts.allowRuleFlag.replace(/^--?/, '')) ? ` (or ${facts.allowRuleFlag})` : '';
|
|
23
|
+
const budgetKnob = typeof facts.budgetSetting === 'string' && SENTENCE_TOOL_NAME.test(facts.budgetSetting) ? facts.budgetSetting : undefined;
|
|
24
|
+
const cause = facts.cause === 'no_allow_rule' || facts.cause === 'ask_rule' || facts.cause === 'budget' ? facts.cause : 'other';
|
|
25
|
+
if (facts.lane === 'dont_ask') {
|
|
26
|
+
const mode = 'denied by the dontAsk permission mode';
|
|
27
|
+
switch (cause) {
|
|
28
|
+
case 'no_allow_rule':
|
|
29
|
+
return `${mode}: ${one} needed approval, no permission rule allowed it, and this mode refuses anything that would ask — calls that need no approval still run; an allow rule in settings${flag} can approve calls like this ahead of time`;
|
|
30
|
+
case 'ask_rule':
|
|
31
|
+
return `${mode}: an ask rule matched ${one}, and this mode refuses anything that would ask (an ask rule outranks allow rules) — narrow or remove that ask rule, or use another permission mode to answer it`;
|
|
32
|
+
case 'budget':
|
|
33
|
+
return `${mode}: ${one} needed approval, and this session's automatic-approval budget is used up, so saved rules no longer approve calls${budgetKnob !== undefined ? ` — raise ${budgetKnob} before launch to change the cap` : ''}`;
|
|
34
|
+
default:
|
|
35
|
+
return `${mode}: ${one} needed a person's approval that saved permission rules cannot approve here (the engine or your deployment requires a person for it, or it could not be checked against your rules), and this mode refuses anything that would ask — use another permission mode to answer it`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const lane = 'no approval UI on this lane';
|
|
39
|
+
const tail = 'nobody can be asked here, so only this call was refused — calls that need no approval still run';
|
|
40
|
+
switch (cause) {
|
|
41
|
+
case 'no_allow_rule':
|
|
42
|
+
return `${lane}: ${one} needed a person's approval, no permission rule answered it, and ${tail}; a matching allow rule in settings${flag} can approve calls like this ahead of time`;
|
|
43
|
+
case 'ask_rule':
|
|
44
|
+
return `${lane}: an ask rule matched ${one}, so it needed a person's answer, and ${tail}; an ask rule outranks allow rules, so narrow or remove that ask rule, or run the task interactively to answer it`;
|
|
45
|
+
case 'budget':
|
|
46
|
+
return `${lane}: ${one} needed a person's approval, and this session's automatic-approval budget is used up, so saved rules no longer approve calls; ${tail}${budgetKnob !== undefined ? `; raise ${budgetKnob} before launch to change the cap` : ''}`;
|
|
47
|
+
default:
|
|
48
|
+
return `${lane}: ${one} needed a person's approval, and saved permission rules cannot approve it here (the engine or your deployment requires a person for it, or it could not be checked against your rules); ${tail}; run the task interactively to answer it`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
17
51
|
function denyOutputForRender(attribution) {
|
|
18
52
|
if (attribution?.settledBy !== 'policy')
|
|
19
53
|
return HITL_REJECT_MESSAGE;
|
|
@@ -232,10 +266,24 @@ const TOOL_END_ARMS = [
|
|
|
232
266
|
'改顺序必须同批改 TOOL_END_ARM_ORDER 并说明为什么新顺序仍然对。');
|
|
233
267
|
}
|
|
234
268
|
}
|
|
269
|
+
function completeFrameParked(ev) {
|
|
270
|
+
try {
|
|
271
|
+
const d = Object.getOwnPropertyDescriptor(ev, 'parked');
|
|
272
|
+
return d !== undefined && 'value' in d && d.value === true;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
235
278
|
async function routeToolApprovalFrame(ev, ctx) {
|
|
236
279
|
const { led, deps } = ctx;
|
|
237
|
-
if (ev.type === 'tool_approval_complete')
|
|
280
|
+
if (ev.type === 'tool_approval_complete') {
|
|
281
|
+
const id = ev.approvalId;
|
|
282
|
+
if (typeof id === 'string' && id !== '' && supersedeLiveCard(liveFrameCallKey(id), completeFrameParked(ev))) {
|
|
283
|
+
hostLog('debug', `liveHitlAskWire: tool_approval_complete for ${id} arrived while its card was waiting — card withdrawn (nothing will be sent from it)`);
|
|
284
|
+
}
|
|
238
285
|
return { kind: 'skip' };
|
|
286
|
+
}
|
|
239
287
|
if (!deps.respondToolApproval) {
|
|
240
288
|
hostLog('debug', `liveHitlAskWire: tool_approval frame ${ev.approvalId} but no respond wire — engine self-settles (TTL deny)`);
|
|
241
289
|
return { kind: 'skip' };
|
|
@@ -269,7 +317,16 @@ async function routeToolApprovalFrame(ev, ctx) {
|
|
|
269
317
|
const frameCallId = !fromSubagent && typeof ev.toolCallId === 'string' && ev.toolCallId !== '' ? ev.toolCallId : undefined;
|
|
270
318
|
const gatedCallId = fromSubagent ? undefined : frameCallId ?? led.lastPendingFsCall();
|
|
271
319
|
const argsCallId = frameCallId === undefined ? gatedCallId : typeof ev.toolName === 'string' && toolNameIsFsWrite(ev.toolName) ? frameCallId : undefined;
|
|
272
|
-
const
|
|
320
|
+
const launched = surfaceToolApprovalFrameAndRespond(ev, deps.respondToolApproval, argsOfGatedStart(led, argsCallId), ctx.signal, deps.approvalLane);
|
|
321
|
+
const hold = liveCardHoldOf(liveFrameCallKey(ev.approvalId));
|
|
322
|
+
const handled = launched.then((outcome) => settleLiveCardOutcome(ev, outcome, ctx, fromSubagent, gatedCallId), (e) => {
|
|
323
|
+
hostLog('debug', `liveHitlAskWire: tool_approval card for ${ev.approvalId} failed: ${String(e).slice(0, 160)}`);
|
|
324
|
+
});
|
|
325
|
+
led.trackLiveCard(() => hold?.phase ?? 'card', handled);
|
|
326
|
+
return { kind: 'skip' };
|
|
327
|
+
}
|
|
328
|
+
function settleLiveCardOutcome(ev, outcome, ctx, fromSubagent, gatedCallId) {
|
|
329
|
+
const { led, deps } = ctx;
|
|
273
330
|
const { decision } = outcome;
|
|
274
331
|
if (deps.onToolApprovalOutcome !== undefined) {
|
|
275
332
|
try {
|
|
@@ -294,7 +351,6 @@ async function routeToolApprovalFrame(ev, ctx) {
|
|
|
294
351
|
else
|
|
295
352
|
led.armDenyStamp(attribution);
|
|
296
353
|
}
|
|
297
|
-
return { kind: 'skip' };
|
|
298
354
|
}
|
|
299
355
|
const REPLAYED_APPROVAL_LIST_DEADLINE_MS = 3000;
|
|
300
356
|
async function judgeLedgerApprovalFrame(ev, ctx) {
|
|
@@ -23,6 +23,9 @@ export interface GateLedger {
|
|
|
23
23
|
notePreambleApproval(approvalId: string): void;
|
|
24
24
|
preambleApprovals(): ReadonlySet<string>;
|
|
25
25
|
beginStreamSegment(): void;
|
|
26
|
+
trackLiveCard(phaseOf: () => 'card' | 'respond', handled: Promise<void>): void;
|
|
27
|
+
answeringLiveCards(): Promise<void>[];
|
|
28
|
+
liveCardsSettled(): Promise<void>;
|
|
26
29
|
markStarted(callId: string): boolean;
|
|
27
30
|
noteGatedStart(callId: string, ev: AgentEvent): void;
|
|
28
31
|
gatedStartArgs(): ReadonlyMap<string, unknown>;
|
package/dist/hitl/gateLedger.js
CHANGED
|
@@ -88,6 +88,7 @@ export function createGateLedger() {
|
|
|
88
88
|
let denyStampAttribution;
|
|
89
89
|
let seq;
|
|
90
90
|
const preambleApprovalIds = new Set();
|
|
91
|
+
const liveCards = new Set();
|
|
91
92
|
return {
|
|
92
93
|
noteSeq(ev) {
|
|
93
94
|
const s = durableSeqOf(ev);
|
|
@@ -120,6 +121,22 @@ export function createGateLedger() {
|
|
|
120
121
|
beginStreamSegment() {
|
|
121
122
|
preambleApprovalIds.clear();
|
|
122
123
|
},
|
|
124
|
+
trackLiveCard(phaseOf, handled) {
|
|
125
|
+
const entry = { phaseOf, handled };
|
|
126
|
+
liveCards.add(entry);
|
|
127
|
+
void handled.then(() => { liveCards.delete(entry); }, () => { liveCards.delete(entry); });
|
|
128
|
+
},
|
|
129
|
+
answeringLiveCards() {
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const e of liveCards)
|
|
132
|
+
if (e.phaseOf() === 'respond')
|
|
133
|
+
out.push(e.handled);
|
|
134
|
+
return out;
|
|
135
|
+
},
|
|
136
|
+
async liveCardsSettled() {
|
|
137
|
+
while (liveCards.size > 0)
|
|
138
|
+
await Promise.allSettled([...liveCards].map((e) => e.handled));
|
|
139
|
+
},
|
|
123
140
|
markStarted(callId) {
|
|
124
141
|
if (startedCalls.has(callId))
|
|
125
142
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
|
|
1
|
+
import type { AgentEvent, ApprovalDecidedBy, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
|
|
2
2
|
import type { PermissionResult } from '@sema-agent/agent-types';
|
|
3
3
|
import type { ApprovalsListEnvelope } from './crashConverged.js';
|
|
4
4
|
import { type SuspendedReopenReading } from './suspendedReopen.js';
|
|
@@ -56,7 +56,7 @@ export interface HitlDecideDenyOutcome {
|
|
|
56
56
|
decision: 'deny';
|
|
57
57
|
reason?: string | undefined;
|
|
58
58
|
settledBy?: 'policy' | undefined;
|
|
59
|
-
decidedBy?: 'policy' | undefined;
|
|
59
|
+
decidedBy?: Extract<ApprovalDecidedBy, 'policy'> | undefined;
|
|
60
60
|
}
|
|
61
61
|
export declare class HitlSafetyError extends Error {
|
|
62
62
|
readonly code: HitlSafetyCode;
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -3,6 +3,7 @@ import { eventSeq } from '../adapter/types.js';
|
|
|
3
3
|
import { hostLog } from '../host.js';
|
|
4
4
|
import { abortableSleep } from '../abortableSleep.js';
|
|
5
5
|
import { readOwnDataValue } from '../ownKey.js';
|
|
6
|
+
import { wireFailureTextsOf } from '../wireFailureShape.js';
|
|
6
7
|
import { suspendedReopenOf } from './suspendedReopen.js';
|
|
7
8
|
export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
|
|
8
9
|
const MAX_DENY_REASON_CHARS = 4096;
|
|
@@ -208,7 +209,9 @@ export async function findPendingForTask(client, taskId, matches, opts, callId,
|
|
|
208
209
|
rows = (await client.approvals.list(opts)).pending;
|
|
209
210
|
}
|
|
210
211
|
catch (e) {
|
|
211
|
-
|
|
212
|
+
const t = wireFailureTextsOf(e);
|
|
213
|
+
hostLog('debug', `hitlBridge: approvals.list failed: ${t.log}`);
|
|
214
|
+
return { ok: false, reason: `approvals.list failed: ${t.reason}` };
|
|
212
215
|
}
|
|
213
216
|
const decidable = rows.filter((r) => askParkForeignGateKind(r) === null);
|
|
214
217
|
const identityRow = callId !== undefined
|
|
@@ -327,13 +330,8 @@ export class HitlBridge {
|
|
|
327
330
|
throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry editedPlan`, 'bad_plan_edit');
|
|
328
331
|
}
|
|
329
332
|
const modeAfter = outcome.permissionModeAfter;
|
|
330
|
-
if (modeAfter !== undefined) {
|
|
331
|
-
|
|
332
|
-
throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry permissionModeAfter (it only applies to an approval)`, 'bad_plan_mode');
|
|
333
|
-
}
|
|
334
|
-
if (!isPlanReviewModeAfter(modeAfter)) {
|
|
335
|
-
throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
|
|
336
|
-
}
|
|
333
|
+
if (modeAfter !== undefined && !isPlanReviewModeAfter(modeAfter)) {
|
|
334
|
+
throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
|
|
337
335
|
}
|
|
338
336
|
const req = { decision: outcome.decision };
|
|
339
337
|
if (modeAfter !== undefined && isPlanReviewModeAfter(modeAfter))
|
|
@@ -416,11 +414,11 @@ export class HitlBridge {
|
|
|
416
414
|
throw e;
|
|
417
415
|
if (isDecideAttemptTimeout(e)) {
|
|
418
416
|
if (attempts >= DECIDE_TIMEOUT_MIN_ATTEMPTS && Date.now() - startedAt >= decideTimeoutRetryBudgetMs()) {
|
|
419
|
-
throw new DecideTransportRetryExhaustedError(attempts,
|
|
417
|
+
throw new DecideTransportRetryExhaustedError(attempts, wireFailureTextsOf(e).reason);
|
|
420
418
|
}
|
|
421
419
|
}
|
|
422
420
|
else if (netRetriesUsed >= 1) {
|
|
423
|
-
throw new DecideTransportRetryExhaustedError(attempts,
|
|
421
|
+
throw new DecideTransportRetryExhaustedError(attempts, wireFailureTextsOf(e).reason);
|
|
424
422
|
}
|
|
425
423
|
else {
|
|
426
424
|
netRetriesUsed++;
|
|
@@ -23,11 +23,17 @@ export declare function hitlHostSurfaceFor(sessionKey: string): HitlHostSurface
|
|
|
23
23
|
export declare function _resetHitlHostSurfaceForTest(): void;
|
|
24
24
|
export declare function surfaceForCurrentSession(): HitlHostSurface | null;
|
|
25
25
|
export declare const CANCEL_DENY_BUDGET_MS = 2000;
|
|
26
|
+
export declare const INTERRUPT_DENY_REASON = "Interrupted by user";
|
|
26
27
|
export declare const CANCEL_DENY_WARN_TEXT = "could not cancel the pending question \u2014 the session may stay locked; the run may need engine-side recovery";
|
|
27
28
|
export declare const REMEMBER_NOT_APPLIED_WARN_TEXT = "this session-wide allow was not remembered by the worker \u2014 you will be asked again for this tool";
|
|
28
29
|
export declare const EDIT_NOT_FORWARDED_WARN_TEXT = "your edited tool input was NOT forwarded \u2014 the tool is running with the ORIGINAL input you saw before editing";
|
|
29
30
|
export declare function surfaceRememberNotApplied(): void;
|
|
30
31
|
export declare function surfaceEditNotForwarded(): void;
|
|
32
|
+
export declare const APPROVAL_PARKING_WARN_TEXT = "This approval is being parked; answer it again on the parked card when it appears.";
|
|
33
|
+
export declare function approvalParkingNoticeText(retryAfterMs?: number): string;
|
|
34
|
+
export declare function surfaceApprovalParking(retryAfterMs?: number): void;
|
|
35
|
+
export declare const COMPACT_NOT_RUNNING_WARN_TEXT = "engine run finished before compaction was applied; nothing was compacted";
|
|
36
|
+
export declare function surfaceCompactNotRunning(sessionKey?: string): void;
|
|
31
37
|
export declare const REPLAYED_APPROVAL_LIST_UNREADABLE_WARN_TEXT: string;
|
|
32
38
|
export declare function surfaceReplayedApprovalListUnreadable(): void;
|
|
33
39
|
export declare const EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT: string;
|
|
@@ -29,22 +29,26 @@ export function _resetHitlHostSurfaceForTest() {
|
|
|
29
29
|
hostSurfaceMissesByKey.clear();
|
|
30
30
|
}
|
|
31
31
|
export function surfaceForCurrentSession() {
|
|
32
|
-
|
|
32
|
+
return surfaceForSession(DEFAULT_SESSION_KEY);
|
|
33
|
+
}
|
|
34
|
+
function surfaceForSession(sessionKey) {
|
|
35
|
+
const hostSurface = hitlHostSurfaceFor(sessionKey);
|
|
33
36
|
if (hostSurface === null) {
|
|
34
|
-
hostSurfaceMissesByKey.set(
|
|
37
|
+
hostSurfaceMissesByKey.set(sessionKey, (hostSurfaceMissesByKey.get(sessionKey) ?? 0) + 1);
|
|
35
38
|
}
|
|
36
39
|
return hostSurface;
|
|
37
40
|
}
|
|
38
41
|
export const CANCEL_DENY_BUDGET_MS = 2000;
|
|
42
|
+
export const INTERRUPT_DENY_REASON = 'Interrupted by user';
|
|
39
43
|
export const CANCEL_DENY_WARN_TEXT = 'could not cancel the pending question — the session may stay locked; the run may need engine-side recovery';
|
|
40
44
|
const CANCEL_DENY_WARN_KEY = 'hitl-cancel-deny-warn';
|
|
41
45
|
const CANCEL_DENY_WARN_TIMEOUT_MS = 10_000;
|
|
42
46
|
function surfaceCancelDenyWarn(text = CANCEL_DENY_WARN_TEXT) {
|
|
43
47
|
surfaceSelfClearingWarn(CANCEL_DENY_WARN_KEY, text, CANCEL_DENY_WARN_TIMEOUT_MS);
|
|
44
48
|
}
|
|
45
|
-
function surfaceSelfClearingWarn(key, text, timeoutMs) {
|
|
49
|
+
function surfaceSelfClearingWarn(key, text, timeoutMs, sessionKey = DEFAULT_SESSION_KEY) {
|
|
46
50
|
try {
|
|
47
|
-
const s =
|
|
51
|
+
const s = surfaceForSession(sessionKey);
|
|
48
52
|
if (!s)
|
|
49
53
|
return;
|
|
50
54
|
s.showNotice({ key, text, color: 'warning', priority: 'immediate', timeoutMs });
|
|
@@ -72,6 +76,23 @@ export function surfaceRememberNotApplied() {
|
|
|
72
76
|
export function surfaceEditNotForwarded() {
|
|
73
77
|
surfaceSelfClearingWarn(EDIT_NOT_FORWARDED_KEY, EDIT_NOT_FORWARDED_WARN_TEXT, EDIT_NOT_FORWARDED_TIMEOUT_MS);
|
|
74
78
|
}
|
|
79
|
+
export const APPROVAL_PARKING_WARN_TEXT = 'This approval is being parked; answer it again on the parked card when it appears.';
|
|
80
|
+
const APPROVAL_PARKING_KEY = 'hitl-approval-parking';
|
|
81
|
+
const APPROVAL_PARKING_TIMEOUT_MS = 15_000;
|
|
82
|
+
export function approvalParkingNoticeText(retryAfterMs) {
|
|
83
|
+
if (retryAfterMs === undefined)
|
|
84
|
+
return APPROVAL_PARKING_WARN_TEXT;
|
|
85
|
+
return `${APPROVAL_PARKING_WARN_TEXT} (the engine suggested waiting about ${Math.ceil(retryAfterMs / 100) / 10} s)`;
|
|
86
|
+
}
|
|
87
|
+
export function surfaceApprovalParking(retryAfterMs) {
|
|
88
|
+
surfaceSelfClearingWarn(APPROVAL_PARKING_KEY, approvalParkingNoticeText(retryAfterMs), APPROVAL_PARKING_TIMEOUT_MS);
|
|
89
|
+
}
|
|
90
|
+
export const COMPACT_NOT_RUNNING_WARN_TEXT = 'engine run finished before compaction was applied; nothing was compacted';
|
|
91
|
+
const COMPACT_NOT_RUNNING_KEY = 'engine-compact-not-running';
|
|
92
|
+
const COMPACT_NOT_RUNNING_TIMEOUT_MS = 10_000;
|
|
93
|
+
export function surfaceCompactNotRunning(sessionKey = DEFAULT_SESSION_KEY) {
|
|
94
|
+
surfaceSelfClearingWarn(COMPACT_NOT_RUNNING_KEY, COMPACT_NOT_RUNNING_WARN_TEXT, COMPACT_NOT_RUNNING_TIMEOUT_MS, sessionKey);
|
|
95
|
+
}
|
|
75
96
|
export const REPLAYED_APPROVAL_LIST_UNREADABLE_WARN_TEXT = "Could not read the pending-approval list, so an approval request read back from this run's event log was not raised as a question, " +
|
|
76
97
|
'and no decision was sent for it.';
|
|
77
98
|
const REPLAYED_APPROVAL_LIST_UNREADABLE_KEY = 'hitl-replayed-approval-list-unreadable';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wireFailureTextsOf } from '../wireFailureShape.js';
|
|
1
2
|
const BASH_TOOL_NAME_FOR_RULES = 'Bash';
|
|
2
3
|
export function parseLocalAllowRule(rule, expectedToolName, deps) {
|
|
3
4
|
if (typeof rule !== 'string' || rule === '') {
|
|
@@ -8,7 +9,7 @@ export function parseLocalAllowRule(rule, expectedToolName, deps) {
|
|
|
8
9
|
value = deps.parseRule(rule);
|
|
9
10
|
}
|
|
10
11
|
catch (e) {
|
|
11
|
-
return { ok: false, error: `rule text did not parse (${
|
|
12
|
+
return { ok: false, error: `rule text did not parse (${wireFailureTextsOf(e).reason})` };
|
|
12
13
|
}
|
|
13
14
|
if (typeof value.toolName !== 'string' || value.toolName === '') {
|
|
14
15
|
return { ok: false, error: 'rule text carries no tool name' };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type NoAllowCause = 'no_allow_rule' | 'ask_rule' | 'budget' | 'other';
|
|
2
|
+
export interface RuleLadderNoAllowFacts {
|
|
3
|
+
readonly allowed?: boolean;
|
|
4
|
+
readonly denied?: boolean;
|
|
5
|
+
readonly askRuleMatched?: boolean;
|
|
6
|
+
readonly budgetExhausted?: boolean;
|
|
7
|
+
readonly noRuleMatched?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function noAllowCauseOf(card: object | null | undefined, ladder: RuleLadderNoAllowFacts | null | undefined): NoAllowCause | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { automaticAnswerVerdict } from './automaticAnswerVerdict.js';
|
|
2
|
+
function ladderSays(ladder, key) {
|
|
3
|
+
if (typeof ladder !== 'object' || ladder === null)
|
|
4
|
+
return false;
|
|
5
|
+
try {
|
|
6
|
+
const d = Object.getOwnPropertyDescriptor(ladder, key);
|
|
7
|
+
return d !== undefined && 'value' in d && d.value === true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function noAllowCauseOf(card, ladder) {
|
|
14
|
+
if (ladderSays(ladder, 'allowed') || ladderSays(ladder, 'denied'))
|
|
15
|
+
return undefined;
|
|
16
|
+
if (ladderSays(ladder, 'askRuleMatched'))
|
|
17
|
+
return 'ask_rule';
|
|
18
|
+
if (ladderSays(ladder, 'budgetExhausted'))
|
|
19
|
+
return 'budget';
|
|
20
|
+
const standing = automaticAnswerVerdict(card, 'standing_rule');
|
|
21
|
+
if (standing.kind === 'must_ask' && (standing.askClass === 'ask_rule' || standing.origin === 'ask_rule'))
|
|
22
|
+
return 'ask_rule';
|
|
23
|
+
if (ladderSays(ladder, 'noRuleMatched') && standing.kind === 'may_answer')
|
|
24
|
+
return 'no_allow_rule';
|
|
25
|
+
return 'other';
|
|
26
|
+
}
|
|
@@ -4,11 +4,12 @@ import { ASK_GONE, CHECKPOINT_UNSUPPORTED_VERSION, DECIDE_WORKFLOW_PARK_FORKED,
|
|
|
4
4
|
import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
|
|
5
5
|
import { hostLog } from '../host.js';
|
|
6
6
|
import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
|
|
7
|
-
import { observeCancelByDeny } from './hitlHostSurface.js';
|
|
7
|
+
import { INTERRUPT_DENY_REASON, observeCancelByDeny } from './hitlHostSurface.js';
|
|
8
8
|
import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
|
|
9
9
|
import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
|
|
10
10
|
import { readDecideReceipt } from '../decideReceipt.js';
|
|
11
11
|
import { decideFailureNote } from '../decideFailureNote.js';
|
|
12
|
+
import { wireFailureTextsOf } from '../wireFailureShape.js';
|
|
12
13
|
export const MAX_GATE_HOPS = 2;
|
|
13
14
|
export function nextHopBudget(prev, round) {
|
|
14
15
|
const total = (prev?.total ?? 0) + 1;
|
|
@@ -139,7 +140,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
139
140
|
});
|
|
140
141
|
const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
|
|
141
142
|
if (answer === null) {
|
|
142
|
-
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason:
|
|
143
|
+
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: INTERRUPT_DENY_REASON }, gatedCallId, undefined, pending), taskId);
|
|
143
144
|
return { kind: 'aborted', gatedCallId };
|
|
144
145
|
}
|
|
145
146
|
let declinedReceipt;
|
|
@@ -170,11 +171,13 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
|
|
|
170
171
|
const currentPending = readDecideCurrentPending(e);
|
|
171
172
|
const wireCode = readWireErrorCode(e);
|
|
172
173
|
const wireRetriable = typeof e === 'object' && e !== null && refusalSaysStillPending(e);
|
|
174
|
+
const failureTexts = wireFailureTextsOf(e);
|
|
175
|
+
hostLog('debug', `liveHitlAskWire: decide failed: ${failureTexts.log}`);
|
|
173
176
|
return {
|
|
174
177
|
kind: 'failed',
|
|
175
178
|
stage: 'decide',
|
|
176
179
|
gatedCallId,
|
|
177
|
-
reason: `decide failed: ${
|
|
180
|
+
reason: `decide failed: ${failureTexts.reason}${decideFailureNote(e, attemptedDecision)}`,
|
|
178
181
|
...(code !== undefined ? { code } : {}),
|
|
179
182
|
...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
|
|
180
183
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { abortableSleep } from '../abortableSleep.js';
|
|
2
|
+
import { wireFailureTextsOf } from '../wireFailureShape.js';
|
|
2
3
|
function parkRowPollDelay(ms, signal) {
|
|
3
4
|
return abortableSleep(ms, signal ?? new AbortController().signal);
|
|
4
5
|
}
|
|
@@ -16,7 +17,7 @@ export async function waitForParkRowBirth(deps) {
|
|
|
16
17
|
return await deps.probe(attempt, signal);
|
|
17
18
|
}
|
|
18
19
|
catch (e) {
|
|
19
|
-
return { kind: 'unborn', reason: `probe threw: ${
|
|
20
|
+
return { kind: 'unborn', reason: `probe threw: ${wireFailureTextsOf(e).reason}` };
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
const probeWithinRemaining = async (attempt, remainingMs) => {
|
|
@@ -44,6 +44,9 @@ export type RulesFailure = {
|
|
|
44
44
|
} | {
|
|
45
45
|
kind: 'cursor-stale';
|
|
46
46
|
message: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'query-refused';
|
|
49
|
+
message: string;
|
|
47
50
|
} | {
|
|
48
51
|
kind: 'too-many-candidates';
|
|
49
52
|
message: string;
|
|
@@ -78,7 +81,9 @@ export type RulesFailure = {
|
|
|
78
81
|
kind: 'error';
|
|
79
82
|
message: string;
|
|
80
83
|
};
|
|
81
|
-
export declare function classifyRulesFailure(e: unknown
|
|
84
|
+
export declare function classifyRulesFailure(e: unknown, context?: {
|
|
85
|
+
readonly cursorSent?: boolean;
|
|
86
|
+
}): RulesFailure;
|
|
82
87
|
export declare const PERSISTED_RULE_BEHAVIORS: readonly RuleBehavior[];
|
|
83
88
|
export declare function persistedRuleBehaviorOf(rule: unknown): RuleBehavior | undefined;
|
|
84
89
|
export declare const PERSISTED_RULE_BEHAVIOR_UNKNOWN = "(unknown)";
|
|
@@ -36,7 +36,7 @@ export function readCcImportRedeemCounts(result) {
|
|
|
36
36
|
}
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
|
-
export function classifyRulesFailure(e) {
|
|
39
|
+
export function classifyRulesFailure(e, context) {
|
|
40
40
|
const { status, errorCode, message, retryAfterSec, committed } = wireFailureShapeOf(e);
|
|
41
41
|
if (errorCode === 'capability.rule_store_required') {
|
|
42
42
|
return { kind: 'lane-unavailable', message };
|
|
@@ -58,8 +58,16 @@ export function classifyRulesFailure(e) {
|
|
|
58
58
|
if (errorCode === 'state.rule_write_failed') {
|
|
59
59
|
return committed === 'no' ? { kind: 'write-not-committed', message } : { kind: 'write-indeterminate', message };
|
|
60
60
|
}
|
|
61
|
-
if (status === 400 && errorCode === 'request.query_invalid')
|
|
62
|
-
|
|
61
|
+
if (status === 400 && errorCode === 'request.query_invalid') {
|
|
62
|
+
let cursorSent;
|
|
63
|
+
try {
|
|
64
|
+
cursorSent = context?.cursorSent;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
cursorSent = undefined;
|
|
68
|
+
}
|
|
69
|
+
return cursorSent === false ? { kind: 'query-refused', message } : { kind: 'cursor-stale', message };
|
|
70
|
+
}
|
|
63
71
|
if (status === 400 && errorCode === 'request.field_invalid')
|
|
64
72
|
return { kind: 'field-refused', message };
|
|
65
73
|
if (status === 400 && errorCode === 'request.body_shape')
|
|
@@ -144,7 +152,7 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
|
|
|
144
152
|
res = await facade.list({ limit: PAGE_LIMIT, ...cleanParams, ...(cursor !== undefined ? { cursor } : {}) }, opts);
|
|
145
153
|
}
|
|
146
154
|
catch (e) {
|
|
147
|
-
const failure = classifyRulesFailure(e);
|
|
155
|
+
const failure = classifyRulesFailure(e, { cursorSent: cursor !== undefined });
|
|
148
156
|
if (failure.kind === 'cursor-stale' && cursor !== undefined && attempt === 0) {
|
|
149
157
|
hostLog('debug', 'persistedRulesWire: rule list cursor was minted on another rev/principal/scope — dropping it and re-listing from the top (never resuming a stale keyset)');
|
|
150
158
|
restart = true;
|
|
@@ -6,7 +6,8 @@ import { engineWireTarget } from '../engineWireTarget.js';
|
|
|
6
6
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
7
7
|
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
8
8
|
import { decideFailureNote } from '../decideFailureNote.js';
|
|
9
|
-
import {
|
|
9
|
+
import { wireFailureTextsOf } from '../wireFailureShape.js';
|
|
10
|
+
import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, resumeContextUnavailableContent, resumeContextUnavailableFromError } from '../resumeRefusalCopy.js';
|
|
10
11
|
import { REQUEST_FIELD_CONFLICT, isUnavailableFaceCode } from '../engineErrorCodes.js';
|
|
11
12
|
import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
|
|
12
13
|
import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
|
|
@@ -309,6 +310,15 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
|
|
|
309
310
|
return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; TaskOutput("${taskId}") shows where the task stands.`;
|
|
310
311
|
}
|
|
311
312
|
}
|
|
313
|
+
function clientOnlyView(wire) {
|
|
314
|
+
if (wire === undefined || wire === null || typeof wire !== 'object')
|
|
315
|
+
return wire;
|
|
316
|
+
return {
|
|
317
|
+
get client() {
|
|
318
|
+
return wire.client;
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
312
322
|
function planReviewDecisionTimedOutOutcome(decision) {
|
|
313
323
|
return (`The plan_review ${decision} was sent, but no answer came back from the engine within the time limit this host allows for it — ` +
|
|
314
324
|
`it may have taken effect (${decision === 'approve' ? 'an approval keeps the engine busy until the resumed work finishes' : 'the engine may still be applying it'}).`);
|
|
@@ -492,12 +502,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
492
502
|
let stillParkedProven = false;
|
|
493
503
|
let statusSeen;
|
|
494
504
|
const wire = opts?.wire;
|
|
495
|
-
const resolved = engineWireFor(wire, DEFAULT_SESSION_KEY, usablePlanReviewWireClient);
|
|
496
|
-
const badMode = permissionModeAfter !== undefined &&
|
|
505
|
+
const resolved = engineWireFor(clientOnlyView(wire), DEFAULT_SESSION_KEY, usablePlanReviewWireClient);
|
|
506
|
+
const badMode = permissionModeAfter !== undefined && !isPlanReviewModeAfter(permissionModeAfter);
|
|
497
507
|
if (badMode) {
|
|
498
|
-
hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter
|
|
508
|
+
hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter must be "default" or "acceptEdits"`);
|
|
499
509
|
effect = 'not_sent';
|
|
500
|
-
outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid
|
|
510
|
+
outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid (it must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
|
|
501
511
|
}
|
|
502
512
|
else if (resolved.kind === 'no-target') {
|
|
503
513
|
hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
|
|
@@ -600,12 +610,15 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
600
610
|
const refusedCode = e?.errorCode;
|
|
601
611
|
const noAnswer = typeof status === 'number' || typeof e !== 'object' ? null : noAnswerKindOf(e);
|
|
602
612
|
if (status === 400 && refusedCode === REQUEST_FIELD_CONFLICT && permissionModeAfter === 'acceptEdits') {
|
|
603
|
-
hostLog('debug',
|
|
613
|
+
hostLog('debug', `planReviewWire: ${decision} + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode`);
|
|
604
614
|
effect = 'still_parked';
|
|
605
615
|
stillParkedProven = true;
|
|
606
616
|
outcome =
|
|
607
|
-
|
|
608
|
-
'
|
|
617
|
+
decision === 'approve'
|
|
618
|
+
? 'The plan approval was NOT applied: the engine refused "auto-accept edits" for this task (it was not started read-only in plan mode, so that option does not apply). ' +
|
|
619
|
+
'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly, and that approving with "manually approve edits" will go through.'
|
|
620
|
+
: `The plan ${decision} was NOT applied: the engine refused the post-approval permission mode that was sent with it (request.field_conflict). ` +
|
|
621
|
+
'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly that the decision did not go through.';
|
|
609
622
|
}
|
|
610
623
|
else if (status === 404 && refusedCode === 'not_found.plan_review') {
|
|
611
624
|
hostLog('debug', `planReviewWire: ${decision} → 404 not_found.plan_review (no pending plan review any more — an earlier decision likely settled it)`);
|
|
@@ -622,6 +635,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
622
635
|
stillParkedCause = 'stored_form_unsupported';
|
|
623
636
|
outcome = `This ${decision} was not applied. ${checkpointUnsupportedVersionContent(cu, { parkKind: 'plan_review', runId: taskId })}`;
|
|
624
637
|
}
|
|
638
|
+
else if (resumeContextUnavailableFromError(e) !== null) {
|
|
639
|
+
const ru = resumeContextUnavailableFromError(e);
|
|
640
|
+
hostLog('debug', `planReviewWire: ${decision} → 409 conflict.resume_context_unavailable (the session context needed to continue this run is not available)`);
|
|
641
|
+
effect = 'not_applied';
|
|
642
|
+
outcome = `This ${decision} was not applied. ${resumeContextUnavailableContent(ru)}`;
|
|
643
|
+
}
|
|
625
644
|
else if (typeof status === 'number' && corruptStoredRowWhere(e) !== undefined) {
|
|
626
645
|
const where = corruptStoredRowWhere(e);
|
|
627
646
|
hostLog('debug', `planReviewWire: ${decision} → ${String(status)} corrupt (stored resume context unreadable)`);
|
|
@@ -643,7 +662,9 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
643
662
|
outcome = planReviewDecisionConnectionLostOutcome(decision);
|
|
644
663
|
}
|
|
645
664
|
else {
|
|
646
|
-
|
|
665
|
+
const failureTexts = wireFailureTextsOf(e);
|
|
666
|
+
hostLog('debug', `planReviewWire: ${decision} could not reach the engine: ${failureTexts.log}`);
|
|
667
|
+
outcome = `The plan_review decision could not reach the engine: ${failureTexts.reason}`;
|
|
647
668
|
}
|
|
648
669
|
}
|
|
649
670
|
}
|