@akagilnc/pi-workflow-roles 0.1.4620 → 0.1.4641
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/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +962 -547
- package/dist/activation-ledger-topology.js +0 -4
- package/dist/gatekeeper-role.js +76 -6
- package/dist/headless-host/production-host.js +923 -508
- package/dist/migrate-book-topology.js +9 -0
- package/dist/pi/role-turn-host.js +0 -3
- package/dist/public-cli/auto-resume.js +26 -0
- package/dist/public-cli/countersign-run.js +31 -10
- package/dist/public-cli/main.js +1456 -1068
- package/dist/public-cli/post-admission.js +101 -7
- package/dist/public-cli/process-cancel.js +57 -0
- package/dist/public-cli/settlement.js +95 -2
- package/dist/public-role-summons.js +19 -0
- package/dist/secretariat-contracts.js +4 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +0 -1
- package/src/activation-ledger-topology.ts +1 -6
- package/src/activation-ledger.ts +5 -236
- package/src/analyst-entry.ts +19 -2
- package/src/analyst-ledger.ts +111 -6
- package/src/archivist-record-entry.ts +3 -3
- package/src/audit-escalation.ts +1 -1
- package/src/gatekeeper-pass-envelope.ts +37 -5
- package/src/gatekeeper-role.ts +125 -20
- package/src/host-contracts.ts +22 -2
- package/src/pi/adapter.ts +5 -1
- package/src/pi/role-turn-host.ts +0 -3
- package/src/public-cli/auto-resume.ts +34 -0
- package/src/public-cli/cli.ts +30 -0
- package/src/public-cli/countersign-run.ts +50 -13
- package/src/public-cli/main.ts +25 -3
- package/src/public-cli/post-admission.ts +173 -6
- package/src/public-cli/process-cancel.ts +75 -0
- package/src/public-cli/settlement.ts +117 -1
- package/src/public-role-summons.ts +30 -4
- package/src/role-envelope.ts +27 -10
- package/src/role-runtime.ts +146 -55
- package/src/secretariat-contracts.ts +18 -0
- package/dist/activation-reconciliation.js +0 -61
- package/src/activation-reconciliation.ts +0 -163
|
@@ -87,6 +87,11 @@ import {
|
|
|
87
87
|
type TypedProviderHttpObservation,
|
|
88
88
|
type WriterLeaseDiagnosticKind,
|
|
89
89
|
} from "./run-lifecycle.ts";
|
|
90
|
+
import {
|
|
91
|
+
processCancelDiagnostic,
|
|
92
|
+
processCancelSignalName,
|
|
93
|
+
type CatchableProcessSignal,
|
|
94
|
+
} from "./process-cancel.ts";
|
|
90
95
|
import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
|
|
91
96
|
import {
|
|
92
97
|
attemptProducedFreshSubmission,
|
|
@@ -119,6 +124,63 @@ import {
|
|
|
119
124
|
TurnDispatchedFailure,
|
|
120
125
|
} from "./auto-resume.ts";
|
|
121
126
|
|
|
127
|
+
/** #855: process-cancel settlement never re-enters auto-resume. */
|
|
128
|
+
function withProcessCancelSkipAutoResume<T extends object>(
|
|
129
|
+
result: T,
|
|
130
|
+
signal: AbortSignal | undefined,
|
|
131
|
+
): T & { skipAutoResume?: true } {
|
|
132
|
+
if (processCancelSignalName(signal) === undefined) {
|
|
133
|
+
return result as T & { skipAutoResume?: true };
|
|
134
|
+
}
|
|
135
|
+
return { ...result, skipAutoResume: true as const };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* #855: turn already started, then a catchable process signal arrived before
|
|
140
|
+
* lawful seal — settle as named cancel failure and skip auto-resume.
|
|
141
|
+
*/
|
|
142
|
+
async function settleProcessCancelAfterTurn<A extends AdmittedRoleInvocation, T extends TerminalResult>(input: {
|
|
143
|
+
admitted: A;
|
|
144
|
+
cancelName: CatchableProcessSignal;
|
|
145
|
+
result: RoleTurnResult;
|
|
146
|
+
adapters: PostAdmissionAdapters<A, T>;
|
|
147
|
+
env: PostAdmissionEnv;
|
|
148
|
+
io: CliIo;
|
|
149
|
+
persistRunState: boolean;
|
|
150
|
+
deferredPersist: { needsPersist?: true } | Record<string, never>;
|
|
151
|
+
invocationScopeId: string | undefined;
|
|
152
|
+
}): Promise<{
|
|
153
|
+
exitCode: number;
|
|
154
|
+
admitted: A;
|
|
155
|
+
terminal?: T;
|
|
156
|
+
skipAutoResume?: true;
|
|
157
|
+
turnDispatched?: true;
|
|
158
|
+
needsPersist?: true;
|
|
159
|
+
}> {
|
|
160
|
+
const failed = await settleAfterTurnStarted(
|
|
161
|
+
input.admitted,
|
|
162
|
+
withEngineDetourInvocationScope(
|
|
163
|
+
{
|
|
164
|
+
timedOut: input.result.timedOut,
|
|
165
|
+
code: input.result.code,
|
|
166
|
+
stderr: input.result.stderr,
|
|
167
|
+
knownDiagnostic: processCancelDiagnostic(input.cancelName),
|
|
168
|
+
},
|
|
169
|
+
input.invocationScopeId,
|
|
170
|
+
),
|
|
171
|
+
input.adapters,
|
|
172
|
+
input.env.principalAuthority,
|
|
173
|
+
input.io,
|
|
174
|
+
input.persistRunState,
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
...failed,
|
|
178
|
+
turnDispatched: true as const,
|
|
179
|
+
skipAutoResume: true as const,
|
|
180
|
+
...input.deferredPersist,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
122
184
|
/**
|
|
123
185
|
* Nested station-child role already finished its own call-local loop.
|
|
124
186
|
* Parent records that failure once and must not auto-resume into a re-summon
|
|
@@ -587,6 +649,38 @@ export async function dispatchPostAdmissionTurn<
|
|
|
587
649
|
const finishAfterTurn = async (result: DispatchOutcome): Promise<DispatchOutcome> => {
|
|
588
650
|
if (afterDispatchApplied) return result;
|
|
589
651
|
afterDispatchApplied = true;
|
|
652
|
+
// #855: re-read cancel before afterDispatch finalization — never present
|
|
653
|
+
// lawful success after a catchable process signal. skipRunStateWrite: run-state
|
|
654
|
+
// may already be terminal from a prior lawful persist; do not reopen as resumable.
|
|
655
|
+
const cancelBeforeFinish = processCancelSignalName(env.signal);
|
|
656
|
+
if (
|
|
657
|
+
cancelBeforeFinish !== undefined &&
|
|
658
|
+
result.terminal !== undefined &&
|
|
659
|
+
isLawfulTypedTerminalOutcome(result.terminal.roleOutcome)
|
|
660
|
+
) {
|
|
661
|
+
result = {
|
|
662
|
+
...(await settleAfterTurnStarted(
|
|
663
|
+
admitted,
|
|
664
|
+
withEngineDetourInvocationScope(
|
|
665
|
+
{
|
|
666
|
+
timedOut: false,
|
|
667
|
+
code: null,
|
|
668
|
+
stderr: "",
|
|
669
|
+
knownDiagnostic: processCancelDiagnostic(cancelBeforeFinish),
|
|
670
|
+
skipRunStateWrite: true,
|
|
671
|
+
},
|
|
672
|
+
request.invocationScopeId,
|
|
673
|
+
),
|
|
674
|
+
adapters,
|
|
675
|
+
env.principalAuthority,
|
|
676
|
+
io,
|
|
677
|
+
persistRunState,
|
|
678
|
+
)),
|
|
679
|
+
turnDispatched: true as const,
|
|
680
|
+
skipAutoResume: true as const,
|
|
681
|
+
...deferredPersist,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
590
684
|
try {
|
|
591
685
|
// #858: an unbound seat may assert its ticket on the existing receipt.
|
|
592
686
|
// Read the original accepted payload; do not rewrite it, infer from prose,
|
|
@@ -788,20 +882,29 @@ export async function dispatchPostAdmissionTurn<
|
|
|
788
882
|
try {
|
|
789
883
|
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
790
884
|
} catch (error) {
|
|
885
|
+
const processCancelName = processCancelSignalName(env.signal);
|
|
791
886
|
const settled = await settleAfterTurnStarted(
|
|
792
887
|
admitted,
|
|
793
888
|
withEngineDetourInvocationScope({
|
|
794
889
|
timedOut: false,
|
|
795
890
|
code: null,
|
|
796
891
|
stderr: "",
|
|
797
|
-
thrown:
|
|
892
|
+
thrown:
|
|
893
|
+
processCancelName === undefined
|
|
894
|
+
? error
|
|
895
|
+
: new Error(processCancelDiagnostic(processCancelName), { cause: error }),
|
|
798
896
|
}, request.invocationScopeId),
|
|
799
897
|
adapters,
|
|
800
898
|
env.principalAuthority,
|
|
801
899
|
io,
|
|
802
900
|
persistRunState,
|
|
803
901
|
);
|
|
804
|
-
return await finishAfterTurn(
|
|
902
|
+
return await finishAfterTurn(
|
|
903
|
+
withProcessCancelSkipAutoResume(
|
|
904
|
+
{ ...settled, turnDispatched: true as const, ...deferredPersist },
|
|
905
|
+
env.signal,
|
|
906
|
+
),
|
|
907
|
+
);
|
|
805
908
|
}
|
|
806
909
|
|
|
807
910
|
// `result.stderr` stays live in memory for the real classification below
|
|
@@ -917,11 +1020,13 @@ export async function dispatchPostAdmissionTurn<
|
|
|
917
1020
|
// retroactively invalidate an acceptance that already sealed this turn
|
|
918
1021
|
// — a resolved 429 observed earlier in the same session is exactly
|
|
919
1022
|
// that.
|
|
1023
|
+
const processCancelName = processCancelSignalName(env.signal);
|
|
920
1024
|
const directHostFailureSignal =
|
|
921
1025
|
result.timedOut
|
|
922
1026
|
|| result.knownFailure !== undefined
|
|
923
1027
|
|| runnerKnownFailure !== undefined
|
|
924
|
-
|| credentialFailure !== undefined
|
|
1028
|
+
|| credentialFailure !== undefined
|
|
1029
|
+
|| processCancelName !== undefined;
|
|
925
1030
|
hostSignalFailed =
|
|
926
1031
|
directHostFailureSignal
|
|
927
1032
|
|| (result.code !== null && result.code !== 0)
|
|
@@ -959,12 +1064,16 @@ export async function dispatchPostAdmissionTurn<
|
|
|
959
1064
|
// where a bare nonzero exit / resolution failure must not be outranked
|
|
960
1065
|
// by someone else's earlier success (#836 r12 class 2).
|
|
961
1066
|
const staleAcceptanceOutranksRealFailure = !settledIsFreshThisAttempt && hostSignalFailed;
|
|
1067
|
+
// #855: re-read cancel after trySettle/attach awaits — a signal in this
|
|
1068
|
+
// window must not land as lawful accepted.
|
|
1069
|
+
const cancelAfterSettle = processCancelSignalName(env.signal);
|
|
962
1070
|
if (
|
|
963
1071
|
settled !== undefined
|
|
964
1072
|
&& shouldPresent(settled)
|
|
965
1073
|
&& !directHostFailureSignal
|
|
966
1074
|
&& !staleAcceptanceOutranksRealFailure
|
|
967
1075
|
&& stderrLogWriteFailure === undefined
|
|
1076
|
+
&& cancelAfterSettle === undefined
|
|
968
1077
|
) {
|
|
969
1078
|
// This court sealed — drop open-court pointer (bare resume no longer continues it).
|
|
970
1079
|
if (
|
|
@@ -1017,6 +1126,24 @@ export async function dispatchPostAdmissionTurn<
|
|
|
1017
1126
|
return await finishAfterTurn({ ...settledFailure, turnDispatched: true as const, ...deferredPersist });
|
|
1018
1127
|
}
|
|
1019
1128
|
if (settledOutcome !== undefined) {
|
|
1129
|
+
// #855: final cancel re-read before lawful seal — settlement window may
|
|
1130
|
+
// have received SIGTERM/SIGINT/SIGHUP after the earlier snapshot.
|
|
1131
|
+
const lateCancel = processCancelSignalName(env.signal);
|
|
1132
|
+
if (lateCancel !== undefined) {
|
|
1133
|
+
return await finishAfterTurn(
|
|
1134
|
+
await settleProcessCancelAfterTurn({
|
|
1135
|
+
admitted,
|
|
1136
|
+
cancelName: lateCancel,
|
|
1137
|
+
result,
|
|
1138
|
+
adapters,
|
|
1139
|
+
env,
|
|
1140
|
+
io,
|
|
1141
|
+
persistRunState,
|
|
1142
|
+
deferredPersist,
|
|
1143
|
+
invocationScopeId: request.invocationScopeId,
|
|
1144
|
+
}),
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1020
1147
|
if (persistRunState) {
|
|
1021
1148
|
try {
|
|
1022
1149
|
await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
|
|
@@ -1045,17 +1172,24 @@ export async function dispatchPostAdmissionTurn<
|
|
|
1045
1172
|
io,
|
|
1046
1173
|
persistRunState,
|
|
1047
1174
|
);
|
|
1048
|
-
return await finishAfterTurn(
|
|
1175
|
+
return await finishAfterTurn(
|
|
1176
|
+
withProcessCancelSkipAutoResume(
|
|
1177
|
+
{ ...failed, turnDispatched: true as const, ...deferredPersist },
|
|
1178
|
+
env.signal,
|
|
1179
|
+
),
|
|
1180
|
+
);
|
|
1049
1181
|
}
|
|
1050
1182
|
}
|
|
1051
1183
|
// Lawful persist + present is the caller's stop seam (auto-resume loop /
|
|
1052
1184
|
// manual resume), not this retried host-turn function.
|
|
1185
|
+
// #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
|
|
1053
1186
|
return await finishAfterTurn({ ...settledOutcome, ...deferredPersist });
|
|
1054
1187
|
}
|
|
1055
1188
|
|
|
1056
1189
|
// Host/runner true failure coexists with already-recorded payloads — never wash as accepted.
|
|
1057
1190
|
if (hostSignalFailed) {
|
|
1058
1191
|
const resolutionInput = controlledFailureInputFromResolution(resolution!);
|
|
1192
|
+
const processCancelName = processCancelSignalName(env.signal);
|
|
1059
1193
|
const stderrLogWriteDetails =
|
|
1060
1194
|
stderrLogWriteFailure === undefined
|
|
1061
1195
|
? undefined
|
|
@@ -1067,6 +1201,9 @@ export async function dispatchPostAdmissionTurn<
|
|
|
1067
1201
|
code: result.code,
|
|
1068
1202
|
stderr: result.stderr,
|
|
1069
1203
|
...resolutionInput,
|
|
1204
|
+
...(processCancelName === undefined
|
|
1205
|
+
? {}
|
|
1206
|
+
: { knownDiagnostic: processCancelDiagnostic(processCancelName) }),
|
|
1070
1207
|
// Secondary fact only — never the cause. Rides on whichever channel
|
|
1071
1208
|
// classification actually reads (knownFailure.details owns it when
|
|
1072
1209
|
// a knownFailure exists; the top-level knownDetails otherwise).
|
|
@@ -1086,7 +1223,12 @@ export async function dispatchPostAdmissionTurn<
|
|
|
1086
1223
|
io,
|
|
1087
1224
|
persistRunState,
|
|
1088
1225
|
);
|
|
1089
|
-
return await finishAfterTurn(
|
|
1226
|
+
return await finishAfterTurn(
|
|
1227
|
+
withProcessCancelSkipAutoResume(
|
|
1228
|
+
{ ...failed, turnDispatched: true as const, ...deferredPersist },
|
|
1229
|
+
env.signal,
|
|
1230
|
+
),
|
|
1231
|
+
);
|
|
1090
1232
|
}
|
|
1091
1233
|
|
|
1092
1234
|
// Nothing else was wrong, but the durable stderr mirror itself failed to
|
|
@@ -1114,6 +1256,23 @@ export async function dispatchPostAdmissionTurn<
|
|
|
1114
1256
|
await settleHostEndedNoReceipt(admitted, env.principalAuthority, courtScope) as T,
|
|
1115
1257
|
courtScope,
|
|
1116
1258
|
);
|
|
1259
|
+
// #855: cancel during no_receipt settlement window is not lawful success.
|
|
1260
|
+
const noReceiptCancel = processCancelSignalName(env.signal);
|
|
1261
|
+
if (noReceiptCancel !== undefined) {
|
|
1262
|
+
return await finishAfterTurn(
|
|
1263
|
+
await settleProcessCancelAfterTurn({
|
|
1264
|
+
admitted,
|
|
1265
|
+
cancelName: noReceiptCancel,
|
|
1266
|
+
result,
|
|
1267
|
+
adapters,
|
|
1268
|
+
env,
|
|
1269
|
+
io,
|
|
1270
|
+
persistRunState,
|
|
1271
|
+
deferredPersist,
|
|
1272
|
+
invocationScopeId: request.invocationScopeId,
|
|
1273
|
+
}),
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1117
1276
|
if (persistRunState) {
|
|
1118
1277
|
try {
|
|
1119
1278
|
await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
|
|
@@ -1137,9 +1296,15 @@ export async function dispatchPostAdmissionTurn<
|
|
|
1137
1296
|
io,
|
|
1138
1297
|
persistRunState,
|
|
1139
1298
|
);
|
|
1140
|
-
return await finishAfterTurn(
|
|
1299
|
+
return await finishAfterTurn(
|
|
1300
|
+
withProcessCancelSkipAutoResume(
|
|
1301
|
+
{ ...failed, turnDispatched: true as const, ...deferredPersist },
|
|
1302
|
+
env.signal,
|
|
1303
|
+
),
|
|
1304
|
+
);
|
|
1141
1305
|
}
|
|
1142
1306
|
}
|
|
1307
|
+
// #855 cancel re-read lives in finishAfterTurn (before afterDispatch).
|
|
1143
1308
|
return await finishAfterTurn({
|
|
1144
1309
|
exitCode: exitCodeForTerminalOutcome(noReceipt.roleOutcome),
|
|
1145
1310
|
admitted,
|
|
@@ -1512,6 +1677,7 @@ export async function runPostAdmissionSeatResume<
|
|
|
1512
1677
|
io: input.io,
|
|
1513
1678
|
sessionAppender: env.sessionAppender,
|
|
1514
1679
|
autoResumeLimit: env.autoResumeLimit,
|
|
1680
|
+
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
1515
1681
|
buildInitialPayload: (): StationChildAttempt => ({ resumeTurn: false }),
|
|
1516
1682
|
buildResumePayload: (): StationChildAttempt => ({ resumeTurn: true }),
|
|
1517
1683
|
// Same as public manual resume: prior-court sealed acceptance is not a
|
|
@@ -1678,6 +1844,7 @@ export async function runPostAdmissionResumable<
|
|
|
1678
1844
|
io,
|
|
1679
1845
|
sessionAppender: env.sessionAppender,
|
|
1680
1846
|
autoResumeLimit: env.autoResumeLimit,
|
|
1847
|
+
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
1681
1848
|
buildInitialPayload: buildScopedInitial,
|
|
1682
1849
|
buildResumePayload: buildScopedResume,
|
|
1683
1850
|
dispatch: async (request, lease, _isFirst, attemptIo) => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #855: catchable process signals for ak-role public entry.
|
|
3
|
+
* SIGTERM / SIGINT / SIGHUP abort the shared controller so hosts can gracefully
|
|
4
|
+
* terminate children (SIGTERM only, never SIGKILL). Uncatchable death (SIGKILL,
|
|
5
|
+
* crash, power loss) is reported via run-state + writer lease on analyst.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CATCHABLE_PROCESS_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"] as const;
|
|
9
|
+
|
|
10
|
+
export type CatchableProcessSignal = (typeof CATCHABLE_PROCESS_SIGNALS)[number];
|
|
11
|
+
|
|
12
|
+
export type ProcessCancelHandle = {
|
|
13
|
+
readonly signal: AbortSignal;
|
|
14
|
+
/** Signal name once a catchable signal has been received; else undefined. */
|
|
15
|
+
readonly receivedSignal: () => CatchableProcessSignal | undefined;
|
|
16
|
+
dispose(): void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Install once per process entry. First catchable signal wins; later signals
|
|
21
|
+
* are ignored so settlement can finish without re-entry storms.
|
|
22
|
+
*/
|
|
23
|
+
export function installProcessCancelHandlers(
|
|
24
|
+
processRef: NodeJS.Process = process,
|
|
25
|
+
): ProcessCancelHandle {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
let received: CatchableProcessSignal | undefined;
|
|
28
|
+
const listeners = new Map<CatchableProcessSignal, () => void>();
|
|
29
|
+
|
|
30
|
+
for (const sig of CATCHABLE_PROCESS_SIGNALS) {
|
|
31
|
+
const onSignal = (): void => {
|
|
32
|
+
if (received !== undefined) return;
|
|
33
|
+
received = sig;
|
|
34
|
+
try {
|
|
35
|
+
controller.abort(sig);
|
|
36
|
+
} catch {
|
|
37
|
+
// already aborted
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
listeners.set(sig, onSignal);
|
|
41
|
+
processRef.on(sig, onSignal);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
receivedSignal: () => received,
|
|
47
|
+
dispose(): void {
|
|
48
|
+
for (const [sig, onSignal] of listeners) {
|
|
49
|
+
processRef.off(sig, onSignal);
|
|
50
|
+
}
|
|
51
|
+
listeners.clear();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Signal name when this AbortSignal was aborted by installProcessCancelHandlers. */
|
|
57
|
+
export function processCancelSignalName(
|
|
58
|
+
signal: AbortSignal | undefined,
|
|
59
|
+
): CatchableProcessSignal | undefined {
|
|
60
|
+
if (signal === undefined || signal.aborted !== true) return undefined;
|
|
61
|
+
const reason = signal.reason;
|
|
62
|
+
if (
|
|
63
|
+
reason === "SIGTERM" ||
|
|
64
|
+
reason === "SIGINT" ||
|
|
65
|
+
reason === "SIGHUP"
|
|
66
|
+
) {
|
|
67
|
+
return reason;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Diagnostic line for a catchable process-signal termination (failure honesty). */
|
|
73
|
+
export function processCancelDiagnostic(signalName: CatchableProcessSignal): string {
|
|
74
|
+
return `ak-role terminated by ${signalName}`;
|
|
75
|
+
}
|
|
@@ -94,6 +94,8 @@ import {
|
|
|
94
94
|
DIARIST_OUTPUT_TOOL_NAME,
|
|
95
95
|
} from "../diarist-contracts.ts";
|
|
96
96
|
import {
|
|
97
|
+
SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY,
|
|
98
|
+
SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
|
|
97
99
|
SECRETARIAT_OUTPUT_TOOL_NAME,
|
|
98
100
|
} from "../secretariat-contracts.ts";
|
|
99
101
|
import {
|
|
@@ -3333,15 +3335,129 @@ export async function trySettleDiaristTerminalResult(
|
|
|
3333
3335
|
return settleLawfulDiaristTerminalResult(admitted, authority, scope);
|
|
3334
3336
|
}
|
|
3335
3337
|
|
|
3338
|
+
/**
|
|
3339
|
+
* #969 seat projection sole authority: 给事中 terminal (署|上呈) booked as a
|
|
3340
|
+
* durable custom entry (envelope-safe) — ledger accepted stays LLM params (#836).
|
|
3341
|
+
* Never scan toolResult rows: those are memory-only on headless/ACP (#617/#959).
|
|
3342
|
+
* Receipt bytes stay original; nested runId rides beside them.
|
|
3343
|
+
* Caller must only apply this when the terminal-defining submission actually
|
|
3344
|
+
* produced the officer entry (gate-bound converged, or 给事中 audit_escalation).
|
|
3345
|
+
*/
|
|
3346
|
+
function countersignTerminalFromEntries(
|
|
3347
|
+
entries: readonly {
|
|
3348
|
+
type?: string;
|
|
3349
|
+
customType?: string;
|
|
3350
|
+
data?: unknown;
|
|
3351
|
+
}[],
|
|
3352
|
+
): import("../secretariat-contracts.ts").SecretariatCountersignTerminalFact | undefined {
|
|
3353
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
3354
|
+
const entry = entries[index];
|
|
3355
|
+
if (entry?.type !== "custom") continue;
|
|
3356
|
+
if (entry.customType !== SECRETARIAT_GATE_OFFICER_ENTRY_TYPE) continue;
|
|
3357
|
+
const data =
|
|
3358
|
+
entry.data !== null && typeof entry.data === "object" && !Array.isArray(entry.data)
|
|
3359
|
+
? (entry.data as Record<string, unknown>)
|
|
3360
|
+
: undefined;
|
|
3361
|
+
if (data?.officer !== "countersign") continue;
|
|
3362
|
+
if (data.receipt === undefined) continue;
|
|
3363
|
+
const runId =
|
|
3364
|
+
typeof data.runId === "string" && data.runId.trim() !== ""
|
|
3365
|
+
? data.runId
|
|
3366
|
+
: undefined;
|
|
3367
|
+
return {
|
|
3368
|
+
receipt: data.receipt,
|
|
3369
|
+
...(runId === undefined ? {} : { runId }),
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
return undefined;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
function withCountersignTerminalFact(
|
|
3376
|
+
roleOutcome: Extract<
|
|
3377
|
+
import("./terminal.ts").TerminalRoleOutcome,
|
|
3378
|
+
{ kind: "accepted" | "audit_escalation" }
|
|
3379
|
+
>,
|
|
3380
|
+
officer: import("../secretariat-contracts.ts").SecretariatCountersignTerminalFact,
|
|
3381
|
+
): typeof roleOutcome {
|
|
3382
|
+
const prior =
|
|
3383
|
+
roleOutcome.decisiveFacts !== undefined && isRecord(roleOutcome.decisiveFacts)
|
|
3384
|
+
? roleOutcome.decisiveFacts
|
|
3385
|
+
: {};
|
|
3386
|
+
return {
|
|
3387
|
+
...roleOutcome,
|
|
3388
|
+
decisiveFacts: {
|
|
3389
|
+
...prior,
|
|
3390
|
+
[SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY]: officer,
|
|
3391
|
+
},
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
/**
|
|
3396
|
+
* Terminal-defining secretariat submission owns officer projection.
|
|
3397
|
+
* Last readable secretariatStatus on accepted payloads decides: only gate-bound
|
|
3398
|
+
* converged projects a prior durable officer entry. Parent escalate bypasses the
|
|
3399
|
+
* gate and must not inherit a stale pass entry from an earlier turn (#969).
|
|
3400
|
+
*/
|
|
3401
|
+
function acceptedSecretariatDefinesOfficerProjection(
|
|
3402
|
+
roleOutcome: Extract<
|
|
3403
|
+
import("./terminal.ts").TerminalRoleOutcome,
|
|
3404
|
+
{ kind: "accepted" }
|
|
3405
|
+
>,
|
|
3406
|
+
): boolean {
|
|
3407
|
+
const payloads = roleOutcome.payloads ?? [];
|
|
3408
|
+
for (let index = payloads.length - 1; index >= 0; index -= 1) {
|
|
3409
|
+
const payload = payloads[index];
|
|
3410
|
+
if (!isRecord(payload)) continue;
|
|
3411
|
+
const status = payload.secretariatStatus;
|
|
3412
|
+
if (typeof status !== "string") continue;
|
|
3413
|
+
return status === "converged";
|
|
3414
|
+
}
|
|
3415
|
+
return false;
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3336
3418
|
async function settleLawfulSecretariatTerminalResult(
|
|
3337
3419
|
admitted: AdmittedSecretariatInvocation,
|
|
3338
3420
|
authority: DurablePrincipalAuthority,
|
|
3339
3421
|
scope?: SettlementCourtScope,
|
|
3340
3422
|
): Promise<TerminalResult | undefined> {
|
|
3341
|
-
|
|
3423
|
+
const settled = await settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
|
|
3342
3424
|
role: "secretariat",
|
|
3343
3425
|
toolName: SECRETARIAT_OUTPUT_TOOL_NAME,
|
|
3344
3426
|
}, scope);
|
|
3427
|
+
if (settled === undefined) return undefined;
|
|
3428
|
+
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
3429
|
+
const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
|
|
3430
|
+
const officer = countersignTerminalFromEntries(entries);
|
|
3431
|
+
if (officer === undefined) return settled;
|
|
3432
|
+
|
|
3433
|
+
// 给事中上呈: public payloads = officer receipt; runId + receipt fact beside.
|
|
3434
|
+
// This kind is produced only when beforeAccept booked the officer entry.
|
|
3435
|
+
if (settled.roleOutcome.kind === "audit_escalation") {
|
|
3436
|
+
return {
|
|
3437
|
+
...settled,
|
|
3438
|
+
roleOutcome: withCountersignTerminalFact(
|
|
3439
|
+
{
|
|
3440
|
+
...settled.roleOutcome,
|
|
3441
|
+
payloads: [officer.receipt],
|
|
3442
|
+
},
|
|
3443
|
+
officer,
|
|
3444
|
+
),
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// Pass (署): keep 中书省 payloads; present 给事中 判词 + runId via decisiveFacts.
|
|
3449
|
+
// Parent escalate accepted terminal must not project a stale prior pass entry.
|
|
3450
|
+
if (settled.roleOutcome.kind === "accepted") {
|
|
3451
|
+
if (!acceptedSecretariatDefinesOfficerProjection(settled.roleOutcome)) {
|
|
3452
|
+
return settled;
|
|
3453
|
+
}
|
|
3454
|
+
return {
|
|
3455
|
+
...settled,
|
|
3456
|
+
roleOutcome: withCountersignTerminalFact(settled.roleOutcome, officer),
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
return settled;
|
|
3345
3461
|
}
|
|
3346
3462
|
|
|
3347
3463
|
/** Try to settle a lawful Secretariat Terminal; undefined only for genuine absence. */
|
|
@@ -1040,9 +1040,9 @@ export async function summonParallelReviewerLenses(options: {
|
|
|
1040
1040
|
return results;
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
|
-
/** Gate officer summons: notary/auditor via --source-run; inspector via pointer
|
|
1043
|
+
/** Gate officer summons: notary/auditor via --source-run; inspector via pointer; countersign via ticket (#969). */
|
|
1044
1044
|
export async function summonGateOfficer(options: {
|
|
1045
|
-
readonly officer: "inspector" | "notary" | "auditor";
|
|
1045
|
+
readonly officer: "inspector" | "notary" | "auditor" | "countersign";
|
|
1046
1046
|
readonly sourceRunDirectory: string;
|
|
1047
1047
|
readonly cwd: string;
|
|
1048
1048
|
readonly home?: string;
|
|
@@ -1052,14 +1052,14 @@ export async function summonGateOfficer(options: {
|
|
|
1052
1052
|
readonly signal?: AbortSignal;
|
|
1053
1053
|
/**
|
|
1054
1054
|
* Plain-language re-ask when the prior officer reply was not three-state (#753 / #756).
|
|
1055
|
-
*
|
|
1055
|
+
* Officers: reviewReask → same-ticket resume summons.instruction.
|
|
1056
1056
|
* Never concatenated into argv (inspector parentRunPath is the pure 卷宗指针).
|
|
1057
1057
|
*/
|
|
1058
1058
|
readonly reask?: string;
|
|
1059
1059
|
/**
|
|
1060
1060
|
* In-flight parent 交卷 body (tool-call arguments). Relayed verbatim as officer
|
|
1061
1061
|
* dialogue content when reask is absent (#879). Binding pointer stays on
|
|
1062
|
-
* --source-run / 卷宗指针 argv — never a content substitute.
|
|
1062
|
+
* --source-run / 卷宗指针 / ticket argv — never a content substitute.
|
|
1063
1063
|
*/
|
|
1064
1064
|
readonly submission?: unknown;
|
|
1065
1065
|
/** Pi-adapter inject — forwarded to summonPublicRole (not a parent-host override). */
|
|
@@ -1121,6 +1121,32 @@ export async function summonGateOfficer(options: {
|
|
|
1121
1121
|
...common,
|
|
1122
1122
|
});
|
|
1123
1123
|
}
|
|
1124
|
+
if (options.officer === "countersign") {
|
|
1125
|
+
// #969: Secretariat submission gate → 给事中.
|
|
1126
|
+
// Dialogue / identity instruction = parent typed payload 原话 (ADR 0079) or reask;
|
|
1127
|
+
// no code-authored summons prose (#924). Ticket identity = parent run durable
|
|
1128
|
+
// board binding only (readBoardTicketNumber) — receipt ticketNumber is payload
|
|
1129
|
+
// content, never the sole identity source; parent true-unbound stays unbound.
|
|
1130
|
+
// correlationId only on this officer — notary/auditor/inspector stay untouched.
|
|
1131
|
+
const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.ts");
|
|
1132
|
+
const correlationId = runIdFromRunDirectory(options.sourceRunDirectory);
|
|
1133
|
+
if (correlationId === undefined || correlationId.trim() === "") {
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`countersign gate summon requires parent runId from sourceRunDirectory: ${options.sourceRunDirectory}`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const { readBoardTicketNumber } = await import("./run-ticket-number.ts");
|
|
1139
|
+
const parentTicket = await readBoardTicketNumber(options.sourceRunDirectory);
|
|
1140
|
+
// Argv instruction = reask or parent payload bytes only (never a fabricated line).
|
|
1141
|
+
const instruction = options.reask ?? gateReviewInstruction ?? "";
|
|
1142
|
+
return summonPublicRole({
|
|
1143
|
+
role: "countersign",
|
|
1144
|
+
argv: ["--project", options.cwd, "--", instruction],
|
|
1145
|
+
...common,
|
|
1146
|
+
correlationId,
|
|
1147
|
+
...(parentTicket === undefined ? {} : { boundTicketNumber: parentTicket }),
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1124
1150
|
// Inspector: argv stays pure 卷宗指针 (#747 parentRunPath lookup key).
|
|
1125
1151
|
// Content (body/reask) rides env → summons.instruction / first-mint prompt.
|
|
1126
1152
|
return summonPublicRole({
|
package/src/role-envelope.ts
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
ENGINE_MODEL_FLAG_NAME,
|
|
10
10
|
normalizeEngineName,
|
|
11
11
|
} from "./engine-detour.ts";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createDefaultGateOfficerSummon,
|
|
14
|
+
requireGatekeeperPass,
|
|
15
|
+
} from "./gatekeeper-pass-envelope.ts";
|
|
13
16
|
import type {
|
|
14
17
|
HostContext,
|
|
15
18
|
HostEventRegistration,
|
|
@@ -229,18 +232,32 @@ export async function prepareRoleEnvelope(options: {
|
|
|
229
232
|
// seat remains reachable through MCP.
|
|
230
233
|
setActiveTools(names) { preferredTools = [...names]; },
|
|
231
234
|
getActiveTools() { return [...preferredTools]; },
|
|
232
|
-
async requireGatekeeperPass(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
async requireGatekeeperPass(gateOptions) {
|
|
236
|
+
const packageRoot =
|
|
237
|
+
typeof options.dependencies.packageRoot === "string"
|
|
238
|
+
? options.dependencies.packageRoot
|
|
239
|
+
: undefined;
|
|
240
|
+
return requireGatekeeperPass({
|
|
241
|
+
context: gateOptions.context,
|
|
242
|
+
subject: gateOptions.subject,
|
|
243
|
+
...(gateOptions.signal === undefined ? {} : { signal: gateOptions.signal }),
|
|
237
244
|
hostActions: {
|
|
238
245
|
failInfrastructure: (error, _context, toolCallId) =>
|
|
239
|
-
|
|
240
|
-
bindSubmissionNonPass:
|
|
246
|
+
gateOptions.hostActions.failInfrastructure(error, gateOptions.context, toolCallId),
|
|
247
|
+
bindSubmissionNonPass: gateOptions.hostActions.bindSubmissionNonPass,
|
|
241
248
|
},
|
|
242
|
-
toolCallId:
|
|
243
|
-
...(
|
|
249
|
+
toolCallId: gateOptions.toolCallId,
|
|
250
|
+
...(gateOptions.submission === undefined ? {} : { submission: gateOptions.submission }),
|
|
251
|
+
// #969: package root + home reach nested 给事中/符宝郎 summons.
|
|
252
|
+
// hostAdapters (when set on deps) forward nested seat selection — production unset.
|
|
253
|
+
summonOfficer: createDefaultGateOfficerSummon({
|
|
254
|
+
cwd: gateOptions.context.cwd ?? request.cwd,
|
|
255
|
+
home: request.home,
|
|
256
|
+
...(packageRoot === undefined ? {} : { packageRoot }),
|
|
257
|
+
...(options.dependencies.hostAdapters === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { hostAdapters: options.dependencies.hostAdapters }),
|
|
260
|
+
}),
|
|
244
261
|
});
|
|
245
262
|
},
|
|
246
263
|
on(...registration: HostEventRegistration) {
|