@akagilnc/pi-workflow-roles 0.1.5216 → 0.1.5241
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/CLAUDE.md +3 -0
- package/README.md +1 -1
- package/dist/acp-host/production-host.js +658 -829
- package/dist/engine-detour-usage.js +4 -16
- package/dist/headless-host/production-host.js +777 -862
- package/dist/host-session-record.js +303 -0
- package/dist/pi/role-turn-host.js +1 -21
- package/dist/public-cli/main.js +200 -487
- package/dist/public-cli/post-admission.js +11 -60
- package/dist/sitian-appender.js +10 -155
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +36 -108
- package/src/engine-detour-usage.ts +4 -16
- package/src/external-host-turn-loop.ts +2 -18
- package/src/headless-host/role-turn-host.ts +67 -31
- package/src/host-contracts.ts +0 -15
- package/src/host-session-record.ts +325 -25
- package/src/notary-role.ts +0 -1
- package/src/pi/role-turn-host.ts +1 -25
- package/src/public-cli/countersign-run.ts +4 -3
- package/src/public-cli/post-admission.ts +10 -69
- package/src/public-role-summons.ts +1 -2
- package/src/role-envelope.ts +2 -2
- package/src/role-runtime.ts +0 -14
- package/src/sitian-appender.ts +16 -205
- package/dist/host-transition-prior-native.js +0 -78
- package/dist/public-cli/case-dossier-delivery.js +0 -109
- package/src/host-transition-prior-native.ts +0 -80
- package/src/pi/pi-normalization.ts +0 -291
- package/src/public-cli/case-dossier-delivery.ts +0 -147
|
@@ -45,8 +45,6 @@ import type {
|
|
|
45
45
|
SessionCustomEntryAppender,
|
|
46
46
|
} from "../host-contracts.ts";
|
|
47
47
|
import { isOfficerReviewSeat } from "../packaged-role-registry.ts";
|
|
48
|
-
import { deliverCaseDossierAsAttachment } from "./case-dossier-delivery.ts";
|
|
49
|
-
|
|
50
48
|
/** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
|
|
51
49
|
function describeCaughtError(error: unknown): { name?: string; message: string; code?: string | number } {
|
|
52
50
|
if (error instanceof Error) {
|
|
@@ -58,8 +56,8 @@ function describeCaughtError(error: unknown): { name?: string; message: string;
|
|
|
58
56
|
|
|
59
57
|
/**
|
|
60
58
|
* Nested gate summons (station child) on an officer seat: dialogue content is
|
|
61
|
-
* peer words only (#879).
|
|
62
|
-
*
|
|
59
|
+
* peer words only (#879). Never RoleTurnRequest.materials; #1092 drops code-side
|
|
60
|
+
* 起居录 path pointer freeze — roles locate records by ticket number.
|
|
63
61
|
*/
|
|
64
62
|
function isStationChildOfficerDialogue(
|
|
65
63
|
role: string,
|
|
@@ -69,10 +67,8 @@ function isStationChildOfficerDialogue(
|
|
|
69
67
|
}
|
|
70
68
|
import {
|
|
71
69
|
mintEngineDetourInvocationScope,
|
|
72
|
-
readInvocationSelectedHost,
|
|
73
70
|
withEngineDetourInvocationScope,
|
|
74
71
|
} from "../engine-detour-usage.ts";
|
|
75
|
-
import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
|
|
76
72
|
import type { CredentialProviders, SeatModelConfig } from "./config.ts";
|
|
77
73
|
import {
|
|
78
74
|
clearCurrentCourt,
|
|
@@ -92,6 +88,7 @@ import {
|
|
|
92
88
|
type CatchableProcessSignal,
|
|
93
89
|
} from "./process-cancel.ts";
|
|
94
90
|
import { homeFromRunDirectory } from "../activation-ledger-topology.ts";
|
|
91
|
+
import { recordRunStart } from "../host-session-record.ts";
|
|
95
92
|
import {
|
|
96
93
|
attemptProducedFreshSubmission,
|
|
97
94
|
classifyPostAdmissionFailure,
|
|
@@ -808,53 +805,11 @@ export async function dispatchPostAdmissionTurn<
|
|
|
808
805
|
try {
|
|
809
806
|
// #987: do not pre-block or classify host dispatch from AK credential
|
|
810
807
|
// catalog facts. Per-invocation runner/host evidence owns provider identity.
|
|
811
|
-
// #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
|
|
812
|
-
// Single authority projectHostTransitionPriorNative classifies the prior native volume.
|
|
813
|
-
// Same-run resume (#637) keeps host identity on the run's invocation page.
|
|
814
|
-
let previousHost: string | undefined;
|
|
815
|
-
const liveHost = env.host;
|
|
816
|
-
const principalCoordinates =
|
|
817
|
-
admitted.principal === undefined
|
|
818
|
-
? undefined
|
|
819
|
-
: env.principalAuthority.decode(admitted.principal);
|
|
820
|
-
let hostTransition: RoleTurnRequest["hostTransition"];
|
|
821
|
-
try {
|
|
822
|
-
previousHost = readInvocationSelectedHost(admitted.runDirectory);
|
|
823
|
-
hostTransition =
|
|
824
|
-
previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
|
|
825
|
-
? await projectHostTransitionPriorNative({
|
|
826
|
-
previousHost,
|
|
827
|
-
liveHost,
|
|
828
|
-
piSessionFile: principalCoordinates.sessionFile,
|
|
829
|
-
})
|
|
830
|
-
: undefined;
|
|
831
|
-
} catch (error) {
|
|
832
|
-
// prior-native IO is on the public one-shot path — controlled failure, not bare throw.
|
|
833
|
-
return {
|
|
834
|
-
...(await presentControlledFailure(
|
|
835
|
-
admitted,
|
|
836
|
-
withEngineDetourInvocationScope({
|
|
837
|
-
timedOut: false,
|
|
838
|
-
code: null,
|
|
839
|
-
stderr: "",
|
|
840
|
-
thrown: error,
|
|
841
|
-
}, request.invocationScopeId),
|
|
842
|
-
adapters,
|
|
843
|
-
env.principalAuthority,
|
|
844
|
-
io,
|
|
845
|
-
persistRunState,
|
|
846
|
-
)) as { exitCode: number; admitted: A; terminal: T },
|
|
847
|
-
...deferredPersist,
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
|
|
851
808
|
await clearTypedProviderHttpObservation(admitted.runDirectory);
|
|
852
809
|
// The authoritative host write (markRunRunning) is delayed to just before
|
|
853
810
|
// executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
|
|
854
|
-
// pre-turn retry (beforeDispatch,
|
|
855
|
-
//
|
|
856
|
-
// committing env.host earlier would make readInvocationSelectedHost read back the
|
|
857
|
-
// new host on the retry and silently drop hostTransition.
|
|
811
|
+
// pre-turn retry (beforeDispatch, continuation assembly) must not commit a host
|
|
812
|
+
// identity until the turn is ready.
|
|
858
813
|
// Failures settle the run (presentControlledFailure); they must not leave it
|
|
859
814
|
// permanently running. Call-local auto-resume retries this hook until it
|
|
860
815
|
// succeeds; only an exhausted nested station child skips the parent loop
|
|
@@ -888,10 +843,8 @@ export async function dispatchPostAdmissionTurn<
|
|
|
888
843
|
|
|
889
844
|
// Turn request is assembled after beforeDispatch so this turn sees whatever it
|
|
890
845
|
// settled — the seat's ticket bind re-projection and any court diarist station
|
|
891
|
-
// writes (#742).
|
|
892
|
-
//
|
|
893
|
-
// (station-child and ordinary share one seam). Dialogue continuation stays
|
|
894
|
-
// caller/peer opaque — never splice system path sections into user dialogue.
|
|
846
|
+
// writes (#742). Dialogue continuation stays caller/peer opaque. #1092: no
|
|
847
|
+
// code-side 起居录 path freeze or readingMaterial inject — roles read by ticket.
|
|
895
848
|
// No package-resume parallel face or typed resume identity.
|
|
896
849
|
// #990: one-shot / auto-resume may freeze principal+runDirectory before
|
|
897
850
|
// relocate; always take durable identity from the live admitted object.
|
|
@@ -903,24 +856,11 @@ export async function dispatchPostAdmissionTurn<
|
|
|
903
856
|
if (env.stationChild !== undefined) {
|
|
904
857
|
turnRequest = { ...turnRequest, stationChild: env.stationChild };
|
|
905
858
|
}
|
|
906
|
-
if (hostTransition !== undefined) {
|
|
907
|
-
turnRequest = { ...turnRequest, hostTransition };
|
|
908
|
-
}
|
|
909
859
|
// Selected host axis rides the shared Host envelope for in-turn tools
|
|
910
860
|
// (detour usage ledger) — never a pre-spawn invocation.json reread.
|
|
911
|
-
if (typeof
|
|
912
|
-
turnRequest = { ...turnRequest, host:
|
|
861
|
+
if (typeof env.host === "string" && env.host.trim() !== "") {
|
|
862
|
+
turnRequest = { ...turnRequest, host: env.host.trim() };
|
|
913
863
|
}
|
|
914
|
-
// 0081 non-dialogue face: freeze pointer section under run/attachments/.
|
|
915
|
-
// Seat consumes via loadCaseDossierReadingMaterial → existing agent-start
|
|
916
|
-
// readingMaterial / systemPrompt.materials fold. Caller instruction, empty
|
|
917
|
-
// request, and resume --message stay verbatim.
|
|
918
|
-
await deliverCaseDossierAsAttachment({
|
|
919
|
-
ticketNumber: admitted.ticketNumber,
|
|
920
|
-
projectRoot: admitted.projectRoot,
|
|
921
|
-
home: env.home,
|
|
922
|
-
runDirectory: admitted.runDirectory,
|
|
923
|
-
});
|
|
924
864
|
|
|
925
865
|
// Authoritative host write happens here, at the real dispatch boundary —
|
|
926
866
|
// immediately before the turn actually starts, after every retryable
|
|
@@ -939,6 +879,7 @@ export async function dispatchPostAdmissionTurn<
|
|
|
939
879
|
|
|
940
880
|
let result: RoleTurnResult;
|
|
941
881
|
try {
|
|
882
|
+
recordRunStart(admitted.runDirectory);
|
|
942
883
|
result = await env.roleTurnHost.executeTurn(turnRequest);
|
|
943
884
|
} catch (error) {
|
|
944
885
|
const processCancelName = processCancelSignalName(env.signal);
|
|
@@ -796,8 +796,7 @@ export async function summonGateOfficer(options: {
|
|
|
796
796
|
// Hard path resolve: fail loud — never fall through to packageMachineHome (#604 / #675).
|
|
797
797
|
home = homeFromRunDirectory(options.sourceRunDirectory);
|
|
798
798
|
}
|
|
799
|
-
// Officer host is seat-owned only (#821)
|
|
800
|
-
// hostTransition consumer in post-admission — not an officer override channel.
|
|
799
|
+
// Officer host is seat-owned only (#821), not a parent override channel.
|
|
801
800
|
// #879: binding pointer = parent run directory; dialogue content = submission body.
|
|
802
801
|
// Conclusion re-ask keeps sole ownership of reviewReask when present.
|
|
803
802
|
let gateReviewInstruction: string | undefined;
|
package/src/role-envelope.ts
CHANGED
|
@@ -677,8 +677,8 @@ export async function prepareRoleEnvelope(options: {
|
|
|
677
677
|
return [value.systemPrompt];
|
|
678
678
|
});
|
|
679
679
|
const systemPromptBody = systemPromptParts.join("\n\n");
|
|
680
|
-
// Typed reading materials from agent-start handlers
|
|
681
|
-
//
|
|
680
|
+
// Typed reading materials from agent-start handlers. Folded into
|
|
681
|
+
// provider-visible systemPrompt at send.
|
|
682
682
|
const readingMaterials: unknown[] = [];
|
|
683
683
|
for (const value of promptResults) {
|
|
684
684
|
if (typeof value !== "object" || value === null) continue;
|
package/src/role-runtime.ts
CHANGED
|
@@ -1256,20 +1256,6 @@ export function createRoleRuntimeExtension(
|
|
|
1256
1256
|
},
|
|
1257
1257
|
};
|
|
1258
1258
|
});
|
|
1259
|
-
// #879: single shared owner of station-child 0081 case-dossier → readingMaterial fold.
|
|
1260
|
-
// post-admission freezes under run/attachments/case-dossier/; this handler alone
|
|
1261
|
-
// projects it onto the existing agent-start materials face (Pi + envelope collect).
|
|
1262
|
-
// Role modules must not re-read the freeze (ADR 0018 lifecycle; no duplicate fold).
|
|
1263
|
-
roleHost.on("before_agent_start", async (_event, ctx) => {
|
|
1264
|
-
const runDir = runDirectoryFromHostContext(ctx);
|
|
1265
|
-
if (runDir === undefined) return;
|
|
1266
|
-
const { loadCaseDossierReadingMaterial } = await import(
|
|
1267
|
-
"./public-cli/case-dossier-delivery.ts"
|
|
1268
|
-
);
|
|
1269
|
-
const caseDossier = await loadCaseDossierReadingMaterial(runDir);
|
|
1270
|
-
if (caseDossier === undefined) return;
|
|
1271
|
-
return { readingMaterial: caseDossier };
|
|
1272
|
-
});
|
|
1273
1259
|
roleHost.on("tool_result", async (event) => {
|
|
1274
1260
|
const role = selectedRole;
|
|
1275
1261
|
if (role === undefined) return;
|
package/src/sitian-appender.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sitian Appender kernel (ADR 0065).
|
|
2
|
+
* Sitian Appender kernel (ADR 0065 / ADR 0086).
|
|
3
3
|
* Computes destination automatically from ledger topology without destination parameters.
|
|
4
|
-
*
|
|
4
|
+
* Log4j-style append-only record sink: appends one row, no deduplication, no read-back, no torn-tail repair.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
appendFileSync,
|
|
9
|
-
existsSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
unlinkSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { appendFileSync } from "node:fs";
|
|
14
8
|
import { dirname, join } from "node:path";
|
|
15
9
|
|
|
16
10
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
@@ -29,59 +23,13 @@ import {
|
|
|
29
23
|
type SitianRecordInput,
|
|
30
24
|
} from "./sitian-contracts.ts";
|
|
31
25
|
|
|
32
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function errorCodeOf(error: unknown): unknown {
|
|
37
|
-
return (error as { code?: unknown }).code;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function identityClaimPath(recordFile: string, identity: string): string {
|
|
41
|
-
const digest = createHash("sha256").update(identity, "utf8").digest("hex");
|
|
42
|
-
return `${recordFile}.id-${digest}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Exclusive create at `path` exactly once across processes.
|
|
47
|
-
* Reuses the package's existing `wx` / O_EXCL zero-content occupancy primitive
|
|
48
|
-
* (same shape as activation-ledger-session / archivist-record-entry).
|
|
49
|
-
*/
|
|
50
|
-
function createExclusiveFile(path: string, contents: string): void {
|
|
51
|
-
writeFileSync(path, contents, { encoding: "utf8", flag: "wx" });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function sealTornTail(recordFile: string): void {
|
|
55
|
-
if (!existsSync(recordFile)) return;
|
|
56
|
-
const buffer = readFileSync(recordFile);
|
|
57
|
-
if (buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
|
|
58
|
-
appendFileSync(recordFile, "\n", "utf8");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
26
|
/** Assign already-recorded lines, preserving their original text and malformed lines. */
|
|
63
27
|
export function appendSitianRecordBlock(input: SitianRecordInput, block: string): void {
|
|
64
28
|
if (block === "") return;
|
|
65
29
|
try {
|
|
66
30
|
const { sessionDir, recordFile, ledgerHome } = resolveSitianRecordPath(input);
|
|
67
31
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
68
|
-
appendFileSync(recordFile,
|
|
69
|
-
sealTornTail(recordFile);
|
|
70
|
-
const identityOf = (line: string): string | undefined => {
|
|
71
|
-
try {
|
|
72
|
-
const row: unknown = JSON.parse(line);
|
|
73
|
-
return isRecord(row) && typeof row.identity === "string" ? row.identity : undefined;
|
|
74
|
-
} catch {
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
const identities = new Set(readFileSync(recordFile, "utf8").split("\n").map(identityOf).filter((id): id is string => id !== undefined));
|
|
79
|
-
for (const line of block.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
80
|
-
const identity = identityOf(line);
|
|
81
|
-
if (identity !== undefined && identities.has(identity)) continue;
|
|
82
|
-
appendFileSync(recordFile, line, "utf8");
|
|
83
|
-
if (identity !== undefined) identities.add(identity);
|
|
84
|
-
}
|
|
32
|
+
appendFileSync(recordFile, block, "utf8");
|
|
85
33
|
} catch (error) {
|
|
86
34
|
if (error instanceof SitianInfrastructureError) throw error;
|
|
87
35
|
throw new SitianInfrastructureError(
|
|
@@ -91,134 +39,6 @@ export function appendSitianRecordBlock(input: SitianRecordInput, block: string)
|
|
|
91
39
|
}
|
|
92
40
|
}
|
|
93
41
|
|
|
94
|
-
function findIdentityPointer(
|
|
95
|
-
recordFile: string,
|
|
96
|
-
identity: string,
|
|
97
|
-
kind: string,
|
|
98
|
-
level: SitianRecord["level"],
|
|
99
|
-
): RecordPointer | undefined {
|
|
100
|
-
if (!existsSync(recordFile)) return undefined;
|
|
101
|
-
const text = readFileSync(recordFile, "utf8");
|
|
102
|
-
for (const line of text.split("\n")) {
|
|
103
|
-
const trimmed = line.trim();
|
|
104
|
-
if (!trimmed) continue;
|
|
105
|
-
try {
|
|
106
|
-
const parsed = JSON.parse(trimmed);
|
|
107
|
-
if (isRecord(parsed) && parsed.identity === identity) {
|
|
108
|
-
return { identity, recordFile, kind, level };
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// Malformed lines (including substate b preserved bad lines) are ignored during self-check
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function unlinkAbsentOk(path: string): Error | undefined {
|
|
118
|
-
try {
|
|
119
|
-
unlinkSync(path);
|
|
120
|
-
return undefined;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
if (errorCodeOf(error) === "ENOENT") return undefined;
|
|
123
|
-
return error instanceof Error ? error : new Error(errorText(error));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Same-identity uniqueness under normal concurrent writers.
|
|
129
|
-
*
|
|
130
|
-
* Exclusive per-identity claim (`wx`) makes check+append atomic for one
|
|
131
|
-
* identity. The winner appends under the claim and always releases it on both
|
|
132
|
-
* normal and throwing exits. A loser that already sees the published row
|
|
133
|
-
* returns that pointer; a loser that hits claim EEXIST without a published row
|
|
134
|
-
* fails as SitianInfrastructureError (knownCause session). Crash residue can
|
|
135
|
-
* leave that one identity blocked — identity-scoped, never a volume lock. No
|
|
136
|
-
* wait loop, write-ahead recovery, PID reclaim, or compare-and-unlink.
|
|
137
|
-
*/
|
|
138
|
-
function appendWithIdentityClaim(
|
|
139
|
-
recordFile: string,
|
|
140
|
-
record: SitianRecord,
|
|
141
|
-
row: string,
|
|
142
|
-
): RecordPointer {
|
|
143
|
-
sealTornTail(recordFile);
|
|
144
|
-
const claimPath = identityClaimPath(recordFile, record.identity);
|
|
145
|
-
const existing = findIdentityPointer(
|
|
146
|
-
recordFile,
|
|
147
|
-
record.identity,
|
|
148
|
-
record.kind,
|
|
149
|
-
record.level,
|
|
150
|
-
);
|
|
151
|
-
if (existing !== undefined) {
|
|
152
|
-
// Best-effort: drop leftover claim after a prior crash-after-append.
|
|
153
|
-
unlinkAbsentOk(claimPath);
|
|
154
|
-
return existing;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
createExclusiveFile(claimPath, "");
|
|
159
|
-
} catch (error) {
|
|
160
|
-
if (errorCodeOf(error) !== "EEXIST") throw error;
|
|
161
|
-
sealTornTail(recordFile);
|
|
162
|
-
const published = findIdentityPointer(
|
|
163
|
-
recordFile,
|
|
164
|
-
record.identity,
|
|
165
|
-
record.kind,
|
|
166
|
-
record.level,
|
|
167
|
-
);
|
|
168
|
-
if (published !== undefined) {
|
|
169
|
-
unlinkAbsentOk(claimPath);
|
|
170
|
-
return published;
|
|
171
|
-
}
|
|
172
|
-
throw new SitianInfrastructureError(
|
|
173
|
-
`Sitian identity claim at ${claimPath} already exists for identity ${record.identity}`,
|
|
174
|
-
{ cause: error },
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let primaryFailure: unknown;
|
|
179
|
-
let result: RecordPointer | undefined;
|
|
180
|
-
let cleanupFailure: Error | undefined;
|
|
181
|
-
try {
|
|
182
|
-
sealTornTail(recordFile);
|
|
183
|
-
const raced = findIdentityPointer(
|
|
184
|
-
recordFile,
|
|
185
|
-
record.identity,
|
|
186
|
-
record.kind,
|
|
187
|
-
record.level,
|
|
188
|
-
);
|
|
189
|
-
if (raced !== undefined) {
|
|
190
|
-
result = raced;
|
|
191
|
-
} else {
|
|
192
|
-
appendFileSync(recordFile, row, "utf8");
|
|
193
|
-
result = {
|
|
194
|
-
identity: record.identity,
|
|
195
|
-
recordFile,
|
|
196
|
-
kind: record.kind,
|
|
197
|
-
level: record.level,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
} catch (error) {
|
|
201
|
-
primaryFailure = error;
|
|
202
|
-
} finally {
|
|
203
|
-
cleanupFailure = unlinkAbsentOk(claimPath);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (primaryFailure !== undefined) {
|
|
207
|
-
if (primaryFailure instanceof SitianInfrastructureError) throw primaryFailure;
|
|
208
|
-
throw new SitianInfrastructureError(
|
|
209
|
-
`Sitian appender persistence failure: ${errorText(primaryFailure)}`,
|
|
210
|
-
{ cause: primaryFailure },
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
if (cleanupFailure !== undefined) {
|
|
214
|
-
throw new SitianInfrastructureError(
|
|
215
|
-
`Sitian identity claim at ${claimPath} could not be released after append for identity ${record.identity}: ${errorText(cleanupFailure)}`,
|
|
216
|
-
{ cause: cleanupFailure },
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
return result as RecordPointer;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
42
|
/** Authorized S4 submission ledger kinds that share a common run submission volume. */
|
|
223
43
|
export const S4_SUBMISSION_LEDGER_KINDS = new Set([
|
|
224
44
|
"candidate",
|
|
@@ -307,20 +127,6 @@ export function resolveSitianRecordPathInLedger(
|
|
|
307
127
|
return { sessionDir, recordFile, ledgerHome };
|
|
308
128
|
}
|
|
309
129
|
|
|
310
|
-
/**
|
|
311
|
-
* Owner-visible ticket 起居录 path shape via the writer ticket joins
|
|
312
|
-
* (ticketProvenanceUnderBookPaths). Ledger leaf is the package-owned `.ak-roles`
|
|
313
|
-
* name (ADR 0048); variable slots keep owner labels; no fake-home path reverse.
|
|
314
|
-
*/
|
|
315
|
-
export function projectTicketRecordsPathShape(): string {
|
|
316
|
-
const { recordFile } = ticketProvenanceUnderBookPaths(
|
|
317
|
-
join("~", ".ak-roles"),
|
|
318
|
-
"<簿>",
|
|
319
|
-
"<票号>",
|
|
320
|
-
);
|
|
321
|
-
return recordFile.replace(/\\/g, "/");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
130
|
/** Compute a write destination from ambient ledger topology (ADR 0065). */
|
|
325
131
|
export function resolveSitianRecordPath(input: SitianRecordInput): SitianRecordPath {
|
|
326
132
|
const ledgerHome =
|
|
@@ -331,11 +137,10 @@ export function resolveSitianRecordPath(input: SitianRecordInput): SitianRecordP
|
|
|
331
137
|
}
|
|
332
138
|
|
|
333
139
|
/**
|
|
334
|
-
* Appends a canonical record to its self-computed volume under the Sitian contract.
|
|
335
|
-
* -
|
|
336
|
-
* -
|
|
337
|
-
*
|
|
338
|
-
* Substate b (malformed): preserved as bad line, check misses, appends new row.
|
|
140
|
+
* Appends a canonical record to its self-computed volume under the Sitian contract (ADR 0086).
|
|
141
|
+
* - Log4j-style append-only record sink: O(1) append, unconditionally writes a new line.
|
|
142
|
+
* - Zero read-back, zero per-append identity check / idempotency deduplication.
|
|
143
|
+
* - Zero torn-tail repair.
|
|
339
144
|
* - Commit point: full JSON string ending with newline.
|
|
340
145
|
*/
|
|
341
146
|
export function appendSitianRecord(input: SitianRecordInput): RecordPointer {
|
|
@@ -362,7 +167,13 @@ export function appendSitianRecord(input: SitianRecordInput): RecordPointer {
|
|
|
362
167
|
};
|
|
363
168
|
|
|
364
169
|
const row = `${JSON.stringify(record)}\n`;
|
|
365
|
-
|
|
170
|
+
appendFileSync(recordFile, row, "utf8");
|
|
171
|
+
return {
|
|
172
|
+
identity: record.identity,
|
|
173
|
+
recordFile,
|
|
174
|
+
kind: record.kind,
|
|
175
|
+
level: record.level,
|
|
176
|
+
};
|
|
366
177
|
} catch (error) {
|
|
367
178
|
if (error instanceof SitianInfrastructureError) throw error;
|
|
368
179
|
throw new SitianInfrastructureError(
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single authority for #617 DK-4 cross-host prior-native projection.
|
|
3
|
-
* Classifies the prior volume into the two record families that exist
|
|
4
|
-
* (Pi native session file / sitian run records), never by host name.
|
|
5
|
-
*/
|
|
6
|
-
import { access, readdir } from "node:fs/promises";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
8
|
-
function isEnoent(error) {
|
|
9
|
-
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
10
|
-
}
|
|
11
|
-
/** Present Pi native session path, or empty when ENOENT. */
|
|
12
|
-
async function listPiNativeRecordPaths(sessionFile) {
|
|
13
|
-
try {
|
|
14
|
-
await access(sessionFile);
|
|
15
|
-
return [sessionFile];
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
if (isEnoent(error))
|
|
19
|
-
return [];
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Present sitian records.jsonl paths under sessionParent topology (#717).
|
|
25
|
-
* resolveSitianRecordPathInLedger writes dirname(sessionParent)/<category>/records.jsonl
|
|
26
|
-
* when sessionParent is inside ledger home — never session.jsonl itself.
|
|
27
|
-
*/
|
|
28
|
-
async function listSitianRecordPaths(sessionParent) {
|
|
29
|
-
const sessionRoot = dirname(sessionParent);
|
|
30
|
-
let entries;
|
|
31
|
-
try {
|
|
32
|
-
entries = await readdir(sessionRoot, { withFileTypes: true });
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
if (isEnoent(error))
|
|
36
|
-
return [];
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
const recordPaths = [];
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
if (!entry.isDirectory())
|
|
42
|
-
continue;
|
|
43
|
-
const recordFile = join(sessionRoot, entry.name, "records.jsonl");
|
|
44
|
-
try {
|
|
45
|
-
await access(recordFile);
|
|
46
|
-
recordPaths.push(recordFile);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (!isEnoent(error))
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
recordPaths.sort();
|
|
54
|
-
return recordPaths;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Project one hostTransition only for a real host switch. Empty native volume
|
|
58
|
-
* still yields a typed switch (empty path list).
|
|
59
|
-
*
|
|
60
|
-
* Pi wrote its own session.jsonl; every other host's run volume is the sitian
|
|
61
|
-
* record set on the live run (ADR 0077 全宿主 session 卷宗统一直写, #717) — the CLI's
|
|
62
|
-
* own journals stay in the operator home and are never copied here.
|
|
63
|
-
*/
|
|
64
|
-
export async function projectHostTransitionPriorNative(input) {
|
|
65
|
-
if (input.previousHost === input.liveHost)
|
|
66
|
-
return undefined;
|
|
67
|
-
if (input.previousHost === "pi") {
|
|
68
|
-
return {
|
|
69
|
-
priorNativeKind: "pi-native",
|
|
70
|
-
priorNativePaths: await listPiNativeRecordPaths(input.piSessionFile),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// Sitian path handoff only — do not read bytes.
|
|
74
|
-
return {
|
|
75
|
-
priorNativeKind: "sitian",
|
|
76
|
-
priorNativePaths: await listSitianRecordPaths(input.piSessionFile),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 系统随案递送起居录路径的中立指针段(ADR 0081:公共入口直接挂案卷,Soul 只说明用途不负责派送;
|
|
3
|
-
* #709 公共入口通用递送与 #742 给事中受理链路同源;#858 告诉 LLM 路径,不另建取号/lookup)。
|
|
4
|
-
* 指针输入沿 ADR 0079(指针是绑定材料由代码精准递送),不把卷宗正文塞进提示词。
|
|
5
|
-
* 只告诉路径:不刷新、不生成、不校验内容、不新增拒收或停工条件、不从 instruction 抽票号。
|
|
6
|
-
* 机器文本仅中立标识材料(ADR 0073),用途说明归角色材料所有。
|
|
7
|
-
*
|
|
8
|
-
* 递送挂载点唯一:`post-admission` 在 beforeDispatch 之后为每个公共入口挂载。
|
|
9
|
-
* 全部公共入口(ordinary 已绑定/未绑定与 station-child)统一走 attachments 冻结 +
|
|
10
|
-
* role-runtime `loadCaseDossierReadingMaterial` → readingMaterial →
|
|
11
|
-
* systemPrompt.materials fold(#879 / #858:对话 instruction、空请求、resume
|
|
12
|
-
* message 保持调用者原文;起居录作独立附件面,不新造 RoleTurnRequest.materials,
|
|
13
|
-
* 不拼进 user-dialogue continuation)。
|
|
14
|
-
*
|
|
15
|
-
* #858: deliver returns typed FrozenAttachment[]; load currently re-derives the
|
|
16
|
-
* freeze leaf under runDirectory because agent-start only has HostContext.runDirectory.
|
|
17
|
-
* Consuming the typed frozenPath across that process boundary needs a design
|
|
18
|
-
* revision of the existing envelope/persistence seam — not a local replica of
|
|
19
|
-
* freezeAttachments' `00-` naming, not a drift throw, not readdir guessing.
|
|
20
|
-
*/
|
|
21
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
22
|
-
import { tmpdir } from "node:os";
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
import { projectTicketRecordsPathShape } from "../sitian-facade.js";
|
|
25
|
-
import { resolveTicketProvenanceVolume } from "../ticket-provenance.js";
|
|
26
|
-
import { freezeAttachmentsIntoRun, } from "./invocation.js";
|
|
27
|
-
/** Bound-ticket section heading (presentation only). */
|
|
28
|
-
const CASE_DOSSIER_SECTION_HEADING = "## 本票起居录(系统随案提供)";
|
|
29
|
-
/** Unbound path-only heading — no 本票 claim when no typed ticket is on the run. */
|
|
30
|
-
const UNBOUND_DOSSIER_PATH_HEADING = "## 起居录路径(系统随案提供)";
|
|
31
|
-
/** Stable freeze key under run/attachments/ for station-child 0081 delivery. */
|
|
32
|
-
const CASE_DOSSIER_ATTACH_KEY = "case-dossier";
|
|
33
|
-
/** Leaf name of the frozen pointer section (content = purpose + paths). */
|
|
34
|
-
const CASE_DOSSIER_ATTACH_FILE = "case-dossier-pointer.md";
|
|
35
|
-
/**
|
|
36
|
-
* Neutral 起居录 path pointer for every public entry (ADR 0081 delivery seam).
|
|
37
|
-
* Always tells the canonical path shape so the seat LLM can read the diary
|
|
38
|
-
* itself (陛下 2026-09-15: 只需要告诉llm这个路径). When a typed ticket identity is
|
|
39
|
-
* already on the run, also project the concrete resolved file. Never claims a
|
|
40
|
-
* volume or「本票」exists when unbound; never extracts a ticket from instruction.
|
|
41
|
-
* Under-book shape comes only from sitian writer joins — no local replica.
|
|
42
|
-
*/
|
|
43
|
-
export async function projectCaseDossierPointerSection(input) {
|
|
44
|
-
if (input.ticketNumber === undefined) {
|
|
45
|
-
return [
|
|
46
|
-
UNBOUND_DOSSIER_PATH_HEADING,
|
|
47
|
-
"",
|
|
48
|
-
`记录卷宗:${projectTicketRecordsPathShape()}`,
|
|
49
|
-
].join("\n");
|
|
50
|
-
}
|
|
51
|
-
const volume = resolveTicketProvenanceVolume(input.ticketNumber, input.projectRoot, input.home);
|
|
52
|
-
return [
|
|
53
|
-
CASE_DOSSIER_SECTION_HEADING,
|
|
54
|
-
"",
|
|
55
|
-
`票号:#${input.ticketNumber}`,
|
|
56
|
-
`记录卷宗:${volume.recordFile}`,
|
|
57
|
-
].join("\n");
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* ADR 0081 delivery for every public entry (#879 / #858): freeze the pointer
|
|
61
|
-
* section through the existing attachments seam. Role-runtime loads that freeze
|
|
62
|
-
* via loadCaseDossierReadingMaterial onto readingMaterial; the envelope then
|
|
63
|
-
* folds it into systemPrompt.materials. Caller / peer dialogue instruction stays
|
|
64
|
-
* opaque; never RoleTurnRequest.materials, never user-dialogue continuation.
|
|
65
|
-
* Always freezes the path pointer (canonical shape, or concrete file when a
|
|
66
|
-
* typed ticket is already bound).
|
|
67
|
-
*/
|
|
68
|
-
export async function deliverCaseDossierAsAttachment(input) {
|
|
69
|
-
const section = await projectCaseDossierPointerSection({
|
|
70
|
-
ticketNumber: input.ticketNumber,
|
|
71
|
-
projectRoot: input.projectRoot,
|
|
72
|
-
home: input.home,
|
|
73
|
-
});
|
|
74
|
-
// Stage in OS temp only — never leave a run-local .case-dossier-stage copy.
|
|
75
|
-
const stagingDir = await mkdtemp(join(tmpdir(), "ak-case-dossier-"));
|
|
76
|
-
try {
|
|
77
|
-
const stagingPath = join(stagingDir, CASE_DOSSIER_ATTACH_FILE);
|
|
78
|
-
await writeFile(stagingPath, `${section}\n`, "utf8");
|
|
79
|
-
return await freezeAttachmentsIntoRun([stagingPath], input.runDirectory, CASE_DOSSIER_ATTACH_KEY);
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
await rm(stagingDir, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Load a previously frozen case-dossier attachment as reading material
|
|
87
|
-
* (existing agent-start / systemPrompt.materials fold — not dialogue prompt,
|
|
88
|
-
* not RoleTurnRequest.materials). Undefined when the run has no such freeze.
|
|
89
|
-
*
|
|
90
|
-
* #858: this join re-derives freezeAttachments' leaf naming because the
|
|
91
|
-
* agent-start handler only receives runDirectory. Replacing it requires
|
|
92
|
-
* threading deliver's typed frozenPath across the existing envelope seam
|
|
93
|
-
* (design revision) — do not copy freeze's `00-` algorithm here again.
|
|
94
|
-
*/
|
|
95
|
-
export async function loadCaseDossierReadingMaterial(runDirectory) {
|
|
96
|
-
const frozenPath = join(runDirectory, "attachments", CASE_DOSSIER_ATTACH_KEY, `00-${CASE_DOSSIER_ATTACH_FILE}`);
|
|
97
|
-
let section;
|
|
98
|
-
try {
|
|
99
|
-
section = await readFile(frozenPath, "utf8");
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
if (error.code === "ENOENT")
|
|
103
|
-
return undefined;
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
if (section.trim() === "")
|
|
107
|
-
return undefined;
|
|
108
|
-
return { kind: "case-dossier-pointer", frozenPath, section };
|
|
109
|
-
}
|