@akagilnc/pi-workflow-roles 0.1.5478 → 0.1.5504

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/acp-host/production-host.js +4945 -4980
  2. package/dist/analyst-gate-cycles-read.js +11 -29
  3. package/dist/analyst-ledger.js +10 -12
  4. package/dist/archivist-record-entry.js +4 -3
  5. package/dist/archivist-record-pointer.js +59 -32
  6. package/dist/atomic-write.js +2 -2
  7. package/dist/auditor-dossier-tool.js +8 -4
  8. package/dist/auditor-soul.js +2 -1
  9. package/dist/collector-config.js +6 -5
  10. package/dist/doctor-evidence.js +5 -1
  11. package/dist/engine-detour-usage.js +4 -10
  12. package/dist/headless-host/description.js +251 -0
  13. package/dist/headless-host/production-host.js +2787 -2658
  14. package/dist/ledger-session-read.js +5 -0
  15. package/dist/migrate-book-topology.js +129 -100
  16. package/dist/navigator-public-session.js +13 -17
  17. package/dist/notary-source-run.js +7 -15
  18. package/dist/packaged-role-registry.js +0 -12
  19. package/dist/pi/durable-principal.js +2 -3
  20. package/dist/pi/role-turn-host.js +27 -13
  21. package/dist/public-cli/instruction-seat-run.js +4 -4
  22. package/dist/public-cli/invocation.js +17 -45
  23. package/dist/public-cli/main.js +1074 -1085
  24. package/dist/public-cli/run-lifecycle.js +17 -47
  25. package/dist/public-cli/settlement.js +28 -128
  26. package/dist/public-cli/terminal.js +0 -12
  27. package/dist/role-run-placement.js +52 -5
  28. package/dist/role-run-relocation.js +39 -2
  29. package/dist/run-terminal-artifacts.js +2 -6
  30. package/dist/run-ticket-number.js +33 -4
  31. package/dist/session-assistant-usage.js +9 -18
  32. package/dist/session-identity.js +3 -4
  33. package/dist/sitian-appender.js +20 -6
  34. package/dist/submission-ledger.js +4 -4
  35. package/dist/ticket-provenance-contracts.js +2 -8
  36. package/dist/ticket-provenance.js +5 -4
  37. package/package.json +1 -1
  38. package/src/acp-host/role-turn-host.ts +2 -2
  39. package/src/analyst-gate-cycles-read.ts +11 -34
  40. package/src/analyst-ledger.ts +14 -14
  41. package/src/archivist-record-entry.ts +4 -3
  42. package/src/archivist-record-pointer.ts +70 -34
  43. package/src/atomic-write.ts +3 -1
  44. package/src/auditor-dossier-tool.ts +8 -4
  45. package/src/auditor-soul.ts +2 -1
  46. package/src/collector-config.ts +5 -3
  47. package/src/doctor-evidence.ts +5 -1
  48. package/src/engine-detour-usage.ts +10 -13
  49. package/src/factory-board.ts +16 -81
  50. package/src/headless-host/description.ts +248 -1
  51. package/src/headless-host/production-host.ts +1 -2
  52. package/src/headless-host/role-turn-host.ts +10 -9
  53. package/src/ledger-session-read.ts +6 -0
  54. package/src/navigator-public-session.ts +13 -18
  55. package/src/navigator-work-context.ts +3 -1
  56. package/src/notary-role.ts +2 -1
  57. package/src/notary-source-run.ts +7 -17
  58. package/src/packaged-role-registry.ts +0 -15
  59. package/src/pi/durable-principal.ts +2 -4
  60. package/src/pi/role-turn-host.ts +35 -12
  61. package/src/public-cli/instruction-seat-run.ts +4 -4
  62. package/src/public-cli/invocation.ts +29 -57
  63. package/src/public-cli/run-lifecycle.ts +20 -43
  64. package/src/public-cli/settlement.ts +30 -173
  65. package/src/public-cli/terminal.ts +0 -60
  66. package/src/role-envelope.ts +4 -2
  67. package/src/role-run-placement.ts +63 -4
  68. package/src/role-run-relocation.ts +39 -2
  69. package/src/role-runtime.ts +40 -59
  70. package/src/run-terminal-artifacts.ts +3 -5
  71. package/src/run-ticket-number.ts +41 -3
  72. package/src/session-assistant-usage.ts +8 -20
  73. package/src/session-identity.ts +3 -4
  74. package/src/sitian-appender.ts +20 -12
  75. package/src/submission-ledger.ts +4 -4
  76. package/src/ticket-provenance-contracts.ts +3 -6
  77. package/src/ticket-provenance.ts +5 -6
  78. package/src/ticket-snapshot.ts +3 -2
  79. package/src/ticket-trajectory.ts +85 -99
@@ -12,12 +12,14 @@ import {
12
12
 
13
13
  import { Value } from "typebox/value";
14
14
  import { sitianReport } from "./sitian-facade.ts";
15
+ import { sitianReportSafe } from "./host-session-record.ts";
15
16
  import { createSubmissionLedgerHost, sealAcceptedSubmission } from "./submission-ledger.ts";
16
17
  import { registerFiledSubmissionTool, type FiledSubmissionBeforeAccept } from "./filed-submission.ts";
17
18
  import type { RoleSubmissionDeclaration } from "./role-submission-declarations.ts";
18
19
 
19
20
  import { activationTraceRecordSchema, namedActivationCause, type ActivationTraceRecord, type ActivationTraceWriter } from "./activation-trace.ts";
20
21
  import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
22
+ import { isSafePositiveTicketNumber, parseTicketNumber } from "./run-ticket-number.ts";
21
23
  import {
22
24
  durableSessionPointer,
23
25
  resolveBookKeyFromGit,
@@ -207,12 +209,9 @@ function decodeReviewerAdmittedInputs(getFlag: (name: string) => unknown): Revie
207
209
  authorityRefs = Object.freeze(parsed as string[]);
208
210
  }
209
211
 
210
- let ticketNumber: number | undefined;
211
212
  const rawTicketNumber = getFlag("ak-review-ticket-number");
212
213
  // Shape-invalid flag values do not abort: omit typed candidate; branch→commit→degrade continues.
213
- if (typeof rawTicketNumber === "string" && /^[1-9]\d*$/.test(rawTicketNumber)) {
214
- ticketNumber = Number(rawTicketNumber);
215
- }
214
+ const ticketNumber = parseTicketNumber(rawTicketNumber);
216
215
 
217
216
  const baseRevision = getFlag("ak-review-base");
218
217
  if (typeof baseRevision !== "string" || !baseRevision.trim()) {
@@ -760,15 +759,8 @@ function readDiaristTicketAssertion(
760
759
  }
761
760
  const raw = submitted.ticketNumber;
762
761
  if (raw === null) return { kind: "true-unbound" };
763
- if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) {
764
- return { kind: "ticket", ticketNumber: raw };
765
- }
766
- if (typeof raw === "string" && /^[1-9]\d*$/.test(raw)) {
767
- const n = Number(raw);
768
- if (Number.isSafeInteger(n) && n >= 1) {
769
- return { kind: "ticket", ticketNumber: n };
770
- }
771
- }
762
+ const ticketNumber = parseTicketNumber(raw);
763
+ if (ticketNumber !== undefined) return { kind: "ticket", ticketNumber };
772
764
  return { kind: "invalid" };
773
765
  }
774
766
 
@@ -802,12 +794,9 @@ function readDiaristRunCoordinates(ctx: HostContext): {
802
794
  readonly boundTicketNumber?: number;
803
795
  } {
804
796
  const coordinates = readRoleRunCoordinates(ctx, "diarist accept");
805
- const bound =
806
- typeof coordinates.admitted.ticketNumber === "number" &&
807
- Number.isSafeInteger(coordinates.admitted.ticketNumber) &&
808
- coordinates.admitted.ticketNumber >= 1
809
- ? coordinates.admitted.ticketNumber
810
- : undefined;
797
+ const bound = isSafePositiveTicketNumber(coordinates.admitted.ticketNumber)
798
+ ? coordinates.admitted.ticketNumber
799
+ : undefined;
811
800
  return {
812
801
  runDirectory: coordinates.runDirectory,
813
802
  projectRoot: coordinates.projectRoot,
@@ -1344,16 +1333,14 @@ export function createRoleRuntimeExtension(
1344
1333
  attemptPointer: receiptAttemptPointer(runPointer, ctx.invocationScopeId),
1345
1334
  });
1346
1335
  envelopeHost.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
1347
- try {
1348
- sitianReport({
1349
- level: "event",
1350
- kind: "no-receipt-lifecycle",
1351
- cwd: ctx.cwd,
1352
- sessionParent: ctx.sessionManager.getSessionFile(),
1353
- payload: facts,
1354
- source: "role-runtime",
1355
- });
1356
- } catch {}
1336
+ sitianReportSafe({
1337
+ level: "event",
1338
+ kind: "no-receipt-lifecycle",
1339
+ cwd: ctx.cwd,
1340
+ sessionParent: ctx.sessionManager.getSessionFile(),
1341
+ payload: facts,
1342
+ source: "role-runtime",
1343
+ });
1357
1344
  }
1358
1345
  }
1359
1346
  return;
@@ -1367,16 +1354,14 @@ export function createRoleRuntimeExtension(
1367
1354
  RECEIPT_DELIVERY_REQUEST_ENTRY,
1368
1355
  scopeId.length > 0 ? { invocationScopeId: scopeId } : undefined,
1369
1356
  );
1370
- try {
1371
- sitianReport({
1372
- level: "event",
1373
- kind: "receipt-delivery",
1374
- cwd: ctx.cwd,
1375
- sessionParent: ctx.sessionManager.getSessionFile(),
1376
- payload: { type: "ak-receipt-delivery-request" },
1377
- source: "role-runtime",
1378
- });
1379
- } catch {}
1357
+ sitianReportSafe({
1358
+ level: "event",
1359
+ kind: "receipt-delivery",
1360
+ cwd: ctx.cwd,
1361
+ sessionParent: ctx.sessionManager.getSessionFile(),
1362
+ payload: { type: "ak-receipt-delivery-request" },
1363
+ source: "role-runtime",
1364
+ });
1380
1365
  envelopeHost.sendMessage({
1381
1366
  customType: "ak-receipt-delivery-prompt",
1382
1367
  content: JSON.stringify(receiptDelivery.deliveryState()),
@@ -1391,16 +1376,14 @@ export function createRoleRuntimeExtension(
1391
1376
  attemptPointer: receiptAttemptPointer(runPointer, ctx.invocationScopeId),
1392
1377
  });
1393
1378
  envelopeHost.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
1394
- try {
1395
- sitianReport({
1396
- level: "event",
1397
- kind: "no-receipt-lifecycle",
1398
- cwd: ctx.cwd,
1399
- sessionParent: ctx.sessionManager.getSessionFile(),
1400
- payload: facts,
1401
- source: "role-runtime",
1402
- });
1403
- } catch {}
1379
+ sitianReportSafe({
1380
+ level: "event",
1381
+ kind: "no-receipt-lifecycle",
1382
+ cwd: ctx.cwd,
1383
+ sessionParent: ctx.sessionManager.getSessionFile(),
1384
+ payload: facts,
1385
+ source: "role-runtime",
1386
+ });
1404
1387
  }
1405
1388
  }
1406
1389
  });
@@ -1765,16 +1748,14 @@ export function createRoleRuntimeExtension(
1765
1748
  subjectKey: work.subjectKey,
1766
1749
  };
1767
1750
  envelopeHost.appendEntry(NAVIGATOR_INVOCATION_ENTRY, data);
1768
- try {
1769
- sitianReport({
1770
- level: "event",
1771
- kind: "attendance",
1772
- cwd: ctx.cwd,
1773
- sessionParent: ctx.sessionManager.getSessionFile(),
1774
- payload: { type: NAVIGATOR_INVOCATION_ENTRY, ...data },
1775
- source: "role-runtime",
1776
- });
1777
- } catch {}
1751
+ sitianReportSafe({
1752
+ level: "event",
1753
+ kind: "attendance",
1754
+ cwd: ctx.cwd,
1755
+ sessionParent: ctx.sessionManager.getSessionFile(),
1756
+ payload: { type: NAVIGATOR_INVOCATION_ENTRY, ...data },
1757
+ source: "role-runtime",
1758
+ });
1778
1759
  }
1779
1760
  const activation = navigatorActivation;
1780
1761
  navigatorAttendance = await dependencies.createNavigatorAttendance({
@@ -10,7 +10,8 @@ import { randomUUID } from "node:crypto";
10
10
  import { readdir, readFile } from "node:fs/promises";
11
11
  import { basename, dirname, join } from "node:path";
12
12
 
13
- import { roleRunArtifactsDirectory } from "./role-run-placement.ts";
13
+ import { parseRunLeaf, roleRunArtifactsDirectory } from "./role-run-placement.ts";
14
+
14
15
  import { isRecord, errorText, isMissingPathError } from "./unknown-value.ts";
15
16
 
16
17
  export const RUN_TERMINAL_REPORT_FILE = "report.json" as const;
@@ -155,10 +156,7 @@ async function listUniqueErrorFallbackPaths(
155
156
  * Sole authority for runDirectory → runId (last `@` split).
156
157
  */
157
158
  export function runIdFromRunDirectory(runDirectory: string): string | undefined {
158
- const name = basename(runDirectory);
159
- const at = name.lastIndexOf("@");
160
- if (at <= 0 || at === name.length - 1) return undefined;
161
- return name.slice(0, at);
159
+ return parseRunLeaf(basename(runDirectory))?.runId;
162
160
  }
163
161
 
164
162
  /**
@@ -14,9 +14,48 @@ import { isEnoent, isRecord } from "./unknown-value.ts";
14
14
  export const MIGRATION_TICKET_DERIVATION_PAGE =
15
15
  "migration-ticket-derivation.json" as const;
16
16
 
17
+ /** Positive-integer contract for display / snapshot inputs; binding uses the safe subset below. */
18
+ export function isPositiveTicketNumber(value: unknown): value is number {
19
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
20
+ }
21
+
17
22
  /** Sole safe-positive ticket invariant (bind / admission / placement / readers). */
18
23
  export function isSafePositiveTicketNumber(value: unknown): value is number {
19
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
24
+ return isPositiveTicketNumber(value) && Number.isSafeInteger(value);
25
+ }
26
+
27
+ /** Shared digit spelling, also used for consumer-specific rejection diagnostics. */
28
+ export function isTicketNumberString(value: unknown): value is string {
29
+ return typeof value === "string" && /^[1-9][0-9]*$/.test(value);
30
+ }
31
+
32
+ /**
33
+ * Ticket spelling shared by placement, migration, and sitian subject routing.
34
+ * A number, or a digit string with no sign, zero-pad, decimal, or `#`.
35
+ * Unsafe integers are unidentified. Leading `#N` stays on readDeclaredTicketNumber.
36
+ */
37
+ export function parseTicketNumber(value: unknown): number | undefined {
38
+ if (isSafePositiveTicketNumber(value)) return value;
39
+ if (!isTicketNumberString(value)) return undefined;
40
+ const parsed = Number(value);
41
+ return isSafePositiveTicketNumber(parsed) ? parsed : undefined;
42
+ }
43
+
44
+ /** Ticket number carried by a sitian subject, when the subject is a ticket. */
45
+ export function sitianSubjectTicketNumber(subject: unknown): number | undefined {
46
+ if (typeof subject === "string" || typeof subject === "number") {
47
+ return parseTicketNumber(subject);
48
+ }
49
+ if (subject !== null && typeof subject === "object" && !Array.isArray(subject)) {
50
+ return parseTicketNumber((subject as { ticketNumber?: unknown }).ticketNumber);
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ /** Ticket-provenance directory id from a sitian subject, when the subject is a ticket. */
56
+ export function ticketNumberFromSitianSubject(subject: unknown): string | undefined {
57
+ const parsed = sitianSubjectTicketNumber(subject);
58
+ return parsed === undefined ? undefined : String(parsed);
20
59
  }
21
60
 
22
61
  /**
@@ -35,8 +74,7 @@ export function readDeclaredTicketNumber(value: unknown): number | undefined {
35
74
  // (word-boundary alone would truncate at the decimal point).
36
75
  const match = /^#?([1-9]\d*)(?!\.\d)(?:\b|$)/.exec(trimmed);
37
76
  if (match === null) return undefined;
38
- const ticketNumber = Number(match[1]);
39
- return isSafePositiveTicketNumber(ticketNumber) ? ticketNumber : undefined;
77
+ return parseTicketNumber(match[1]);
40
78
  }
41
79
 
42
80
  /** Loud reject for non-safe-positive ticket numbers before placement or bind. */
@@ -3,7 +3,8 @@
3
3
  * Accumulates real cost when rows carry it; never invents cost zeros (#675).
4
4
  */
5
5
  import type { Usage } from "@earendil-works/pi-ai";
6
- import { join } from "node:path";
6
+ import { ROLE_RUN_SESSION_FILENAME, sessionFileOf } from "./role-run-placement.ts";
7
+ import { readStrictPiSessionJsonl } from "./ledger-session-read.ts";
7
8
 
8
9
  import type { PublicSummonResult } from "./public-role-summons.ts";
9
10
 
@@ -12,25 +13,12 @@ import { errorText } from "./unknown-value.ts";
12
13
  export async function readAssistantUsageFromSessionFile(
13
14
  sessionFile: string,
14
15
  ): Promise<Usage | undefined> {
15
- const { readFile } = await import("node:fs/promises");
16
- let text: string;
17
- try {
18
- text = await readFile(sessionFile, "utf8");
19
- } catch (error) {
20
- if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
21
- throw error;
22
- }
23
16
  let rows: Array<{ type?: string; message?: { role?: string; usage?: Usage } }>;
24
17
  try {
25
- rows = text
26
- .trim()
27
- .split("\n")
28
- .filter(Boolean)
29
- .map((line) => JSON.parse(line) as {
30
- type?: string;
31
- message?: { role?: string; usage?: Usage };
32
- });
18
+ rows = await readStrictPiSessionJsonl(sessionFile) as typeof rows;
33
19
  } catch (error) {
20
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
21
+ if (!(error instanceof SyntaxError)) throw error;
34
22
  throw new Error(
35
23
  `session usage parse failed (${sessionFile}): ${errorText(error)}`,
36
24
  { cause: error },
@@ -95,18 +83,18 @@ export function sessionFileFromPublicSummon(
95
83
  summoned: PublicSummonResult,
96
84
  ): string | undefined {
97
85
  if (typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== "") {
98
- return join(summoned.runDirectory, "session", "session.jsonl");
86
+ return sessionFileOf(summoned.runDirectory);
99
87
  }
100
88
  const fromArtifacts = summoned.terminal?.artifacts
101
89
  ?.map((a) => (a as { path?: string }).path)
102
- .find((p): p is string => typeof p === "string" && p.endsWith("session.jsonl"));
90
+ .find((p): p is string => typeof p === "string" && p.endsWith(ROLE_RUN_SESSION_FILENAME));
103
91
  if (fromArtifacts !== undefined) return fromArtifacts;
104
92
  const outcome = summoned.terminal?.roleOutcome;
105
93
  if (outcome === undefined) return undefined;
106
94
  const facts = (outcome as { decisiveFacts?: Record<string, unknown> }).decisiveFacts;
107
95
  const pointer = facts?.runPointer;
108
96
  if (typeof pointer === "string" && pointer.trim() !== "") {
109
- return join(pointer, "session", "session.jsonl");
97
+ return sessionFileOf(pointer);
110
98
  }
111
99
  return undefined;
112
100
  }
@@ -1,6 +1,7 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
+ import { writeFileAtomically } from "./atomic-write.ts";
4
5
  import type { DurablePrincipal, DurablePrincipalAuthority } from "./host-contracts.ts";
5
6
  import {
6
7
  DEFAULT_ROLE_TURN_HOST,
@@ -35,9 +36,7 @@ export function createSessionIdentityAuthority(
35
36
  async bind(principal, sessionId) {
36
37
  const target = bindingPath(principal);
37
38
  await mkdir(dirname(target), { recursive: true });
38
- const temporary = `${target}.${process.pid}.tmp`;
39
- await writeFile(temporary, `${JSON.stringify({ sessionId })}\n`, { encoding: "utf8", mode: 0o600 });
40
- await rename(temporary, target);
39
+ await writeFileAtomically(target, `${JSON.stringify({ sessionId })}\n`, { mode: 0o600 });
41
40
  },
42
41
  };
43
42
  }
@@ -7,6 +7,9 @@ import { randomUUID } from "node:crypto";
7
7
  import { appendFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
 
10
+ import { sessionDirectoryOf } from "./role-run-placement.ts";
11
+ import { ticketNumberFromSitianSubject } from "./run-ticket-number.ts";
12
+
10
13
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
11
14
  import {
12
15
  activationBookDirectory,
@@ -65,6 +68,18 @@ type SitianRecordPath = {
65
68
  /** Sole records leaf under every sitian volume directory. */
66
69
  const SITIAN_RECORDS_LEAF = "records.jsonl" as const;
67
70
 
71
+ export function sitianVolumeDirectory(sessionDirectory: string, category: string): string {
72
+ return join(sessionDirectory, category);
73
+ }
74
+
75
+ export function sitianRunVolumeDirectory(runDirectory: string, category: string): string {
76
+ return sitianVolumeDirectory(sessionDirectoryOf(runDirectory), category);
77
+ }
78
+
79
+ export function sitianVolumeRecordsFile(volumeDirectory: string): string {
80
+ return join(volumeDirectory, SITIAN_RECORDS_LEAF);
81
+ }
82
+
68
83
  /**
69
84
  * Ticket-provenance under-book paths (docs/dossier-topology.md sole authority).
70
85
  * Writer ticket branch and owner-visible shape share this join — one code path.
@@ -75,7 +90,7 @@ function ticketProvenanceUnderBookPaths(
75
90
  ticketId: string,
76
91
  ): { sessionDir: string; recordFile: string } {
77
92
  const sessionDir = join(activationBookDirectory(ledgerHome, bookKey), ticketId);
78
- return { sessionDir, recordFile: join(sessionDir, SITIAN_RECORDS_LEAF) };
93
+ return { sessionDir, recordFile: sitianVolumeRecordsFile(sessionDir) };
79
94
  }
80
95
 
81
96
  /** Pure topology owner shared by ambient writes and explicit-home submission reads. */
@@ -85,14 +100,7 @@ export function resolveSitianRecordPathInLedger(
85
100
  ): SitianRecordPath {
86
101
  const category = resolveSitianVolumeCategory(input.kind);
87
102
  const ticketNumber = category === "ticket-provenance"
88
- ? typeof input.subject === "string" && /^[1-9][0-9]*$/.test(input.subject)
89
- ? input.subject
90
- : typeof input.subject === "object"
91
- && typeof input.subject.ticketNumber === "number"
92
- && Number.isSafeInteger(input.subject.ticketNumber)
93
- && input.subject.ticketNumber > 0
94
- ? String(input.subject.ticketNumber)
95
- : undefined
103
+ ? ticketNumberFromSitianSubject(input.subject)
96
104
  : undefined;
97
105
 
98
106
  let sessionDir: string;
@@ -108,7 +116,7 @@ export function resolveSitianRecordPathInLedger(
108
116
  recordFile = paths.recordFile;
109
117
  } else if (category === "ticket-provenance" && input.runDirectory !== undefined) {
110
118
  sessionDir = input.runDirectory;
111
- recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
119
+ recordFile = sitianVolumeRecordsFile(sessionDir);
112
120
  } else {
113
121
  if (
114
122
  input.sessionParent === undefined
@@ -117,8 +125,8 @@ export function resolveSitianRecordPathInLedger(
117
125
  ) {
118
126
  throw new Error("Sitian record ownership requires a parent session inside the ledger home");
119
127
  }
120
- sessionDir = join(dirname(input.sessionParent), category);
121
- recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
128
+ sessionDir = sitianVolumeDirectory(dirname(input.sessionParent), category);
129
+ recordFile = sitianVolumeRecordsFile(sessionDir);
122
130
  }
123
131
  if (!physicallyContainedIn(ledgerHome, sessionDir)) {
124
132
  throw new Error("Sitian record ownership requires a directory inside the ledger home");
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { sessionFileIn, sessionFileOf } from "./role-run-placement.ts";
2
2
 
3
3
  import {
4
4
  resolveActivationLedgerHome,
@@ -133,7 +133,7 @@ async function submissionRecordFile(cwd: string, runId: string, scope: Submissio
133
133
  if (sessionParent === undefined) {
134
134
  const discoveredRun = await findRunDirectoryById(scope.home, runId);
135
135
  if (discoveredRun === undefined) return undefined;
136
- sessionParent = join(discoveredRun, "session", "session.jsonl");
136
+ sessionParent = sessionFileOf(discoveredRun);
137
137
  }
138
138
  return resolveSitianRecordPathInLedger({
139
139
  level: "event",
@@ -432,12 +432,12 @@ async function restoreState(cwd: string, runId: string, scope: SubmissionLedgerR
432
432
  /** Sole HostContext-derived session parent for ledger restore/append (never process.env). */
433
433
  function sessionParentFromHostContext(context: HostContext): string | undefined {
434
434
  const runDirectory = runDirectoryFromHostContext(context);
435
- if (runDirectory !== undefined) return join(runDirectory, "session", "session.jsonl");
435
+ if (runDirectory !== undefined) return sessionFileOf(runDirectory);
436
436
  const sessionFile = context.sessionManager.getSessionFile?.();
437
437
  if (typeof sessionFile === "string" && sessionFile.length > 0) return sessionFile;
438
438
  const sessionDir = context.sessionManager.getSessionDir?.();
439
439
  if (typeof sessionDir === "string" && sessionDir.length > 0) {
440
- return join(sessionDir, "session.jsonl");
440
+ return sessionFileIn(sessionDir);
441
441
  }
442
442
  return undefined;
443
443
  }
@@ -1,4 +1,6 @@
1
+ import { parseTicketNumber } from "./run-ticket-number.ts";
1
2
  import { isRecord } from "./unknown-value.ts";
3
+
2
4
  /**
3
5
  * 起居录(ticket-provenance)typed 形状 —— ADR 0075 / ADR 0081 / #1090。
4
6
  *
@@ -63,12 +65,7 @@ export type TicketProvenanceLine = {
63
65
  };
64
66
 
65
67
  function positiveInteger(value: unknown): number | undefined {
66
- if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) return value;
67
- if (typeof value === "string" && /^[1-9][0-9]*$/.test(value)) {
68
- const parsed = Number(value);
69
- if (Number.isSafeInteger(parsed)) return parsed;
70
- }
71
- return undefined;
68
+ return parseTicketNumber(value);
72
69
  }
73
70
 
74
71
  function projectBound(value: unknown): TicketProvenanceBound | undefined {
@@ -4,7 +4,7 @@
4
4
  * 纯追加、不回读历史去重、不折叠。
5
5
  */
6
6
  import { readFile, unlink } from "node:fs/promises";
7
- import { basename, dirname, join, resolve } from "node:path";
7
+ import { join, resolve } from "node:path";
8
8
 
9
9
  import {
10
10
  errnoCode,
@@ -17,6 +17,8 @@ import {
17
17
  type LedgerSessionLine,
18
18
  } from "./ledger-session-read.ts";
19
19
  import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
20
+ import { runDirectoryOfSessionFile, sessionFileOf } from "./role-run-placement.ts";
21
+ import { sitianVolumeRecordsFile } from "./sitian-appender.ts";
20
22
  import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.ts";
21
23
  import {
22
24
  appendSitianRecordBlock,
@@ -76,10 +78,7 @@ function isLedgerRoleSessionFile(absolute: string, home?: string): boolean {
76
78
  : packageMachineHome();
77
79
  const ledgerHome = resolveActivationLedgerHome(machineHome);
78
80
  if (!physicallyContainedIn(ledgerHome, absolute)) return false;
79
- return (
80
- basename(absolute) === "session.jsonl" &&
81
- basename(dirname(absolute)) === "session"
82
- );
81
+ return sessionFileOf(runDirectoryOfSessionFile(absolute)) === absolute;
83
82
  }
84
83
 
85
84
  /** Real I/O seam gate: only host session stores or sitian role-run session.jsonl. */
@@ -152,7 +151,7 @@ export async function rehomeUnboundTicketProvenance(
152
151
  cwd: string,
153
152
  home: string,
154
153
  ): Promise<void> {
155
- const source = join(runDirectory, "records.jsonl");
154
+ const source = sitianVolumeRecordsFile(runDirectory);
156
155
  let content: string;
157
156
  try {
158
157
  content = await readFile(source, "utf8");
@@ -14,6 +14,7 @@
14
14
  import { createGhApiRunner, type GhApiRunner } from "./gh-api-runner.ts";
15
15
 
16
16
  import { isRecord } from "./unknown-value.ts";
17
+ import { isPositiveTicketNumber } from "./run-ticket-number.ts";
17
18
 
18
19
  export type BookRepoBinding = {
19
20
  bookKey: string;
@@ -171,7 +172,7 @@ function parseClosedAt(raw: unknown, label: string): string | null {
171
172
  function parseTicketNode(raw: unknown, label: string): ParsedTicketNode {
172
173
  if (!isRecord(raw)) throw new Error(`${label} is not an object`);
173
174
  const number = raw.number;
174
- if (typeof number !== "number" || !Number.isInteger(number) || number < 1) {
175
+ if (!isPositiveTicketNumber(number)) {
175
176
  throw new Error(`${label}.number invalid`);
176
177
  }
177
178
  if (typeof raw.title !== "string") throw new Error(`${label}.title missing`);
@@ -536,7 +537,7 @@ export function createGhTicketSnapshotTransport(
536
537
  const closedNumbers = [
537
538
  ...new Set(
538
539
  input.closedIssueNumbers.filter(
539
- (n) => Number.isInteger(n) && n > 0 && !tickets.has(n),
540
+ (n) => isPositiveTicketNumber(n) && !tickets.has(n),
540
541
  ),
541
542
  ),
542
543
  ].sort((a, b) => a - b);