@akagilnc/pi-workflow-roles 0.1.5775 → 0.1.5797

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 (43) hide show
  1. package/dist/acp-host/production-host.js +556 -209
  2. package/dist/external-host-turn-loop.js +3 -0
  3. package/dist/headless-host/production-host.js +529 -182
  4. package/dist/migrate-book-topology.js +63 -23
  5. package/dist/pi/role-turn-host.js +6 -1
  6. package/dist/public-cli/instruction-seat-run.js +10 -7
  7. package/dist/public-cli/invocation.js +95 -39
  8. package/dist/public-cli/main.js +357 -147
  9. package/dist/public-cli/post-admission.js +2 -0
  10. package/dist/public-cli/run-lifecycle.js +10 -4
  11. package/dist/public-cli/turn-request.js +3 -0
  12. package/dist/report-ticket-tool.js +3 -3
  13. package/dist/role-run-placement.js +14 -2
  14. package/dist/role-run-relocation.js +2 -2
  15. package/dist/run-dossier.js +5 -4
  16. package/dist/session-identity.js +3 -2
  17. package/dist/sitian-appender.js +12 -3
  18. package/dist/submission-ledger.js +24 -0
  19. package/dist/ticket-progress.js +306 -0
  20. package/dist/user-dialogue-stdin.js +23 -17
  21. package/package.json +1 -1
  22. package/src/external-host-turn-loop.ts +7 -1
  23. package/src/host-contracts.ts +1 -1
  24. package/src/pi/adapter.ts +57 -45
  25. package/src/pi/role-turn-host.ts +6 -1
  26. package/src/prepared-role-turn.ts +6 -0
  27. package/src/public-cli/instruction-seat-run.ts +10 -7
  28. package/src/public-cli/invocation.ts +116 -38
  29. package/src/public-cli/post-admission.ts +3 -1
  30. package/src/public-cli/run-lifecycle.ts +10 -8
  31. package/src/public-cli/turn-request.ts +3 -0
  32. package/src/report-ticket-tool.ts +7 -3
  33. package/src/role-envelope.ts +5 -0
  34. package/src/role-run-placement.ts +19 -2
  35. package/src/role-run-relocation.ts +2 -2
  36. package/src/role-runtime.ts +14 -3
  37. package/src/run-dossier.ts +5 -3
  38. package/src/run-terminal-artifacts.ts +0 -1
  39. package/src/session-identity.ts +3 -2
  40. package/src/sitian-appender.ts +14 -3
  41. package/src/submission-ledger.ts +27 -0
  42. package/src/ticket-progress.ts +383 -0
  43. package/src/user-dialogue-stdin.ts +21 -14
@@ -129,7 +129,10 @@ export async function recordTurnDelivery(
129
129
  }
130
130
  }
131
131
 
132
- export type ExternalPreparedTurn = Pick<PreparedRoleTurn, "prompt" | "abortSignal" | "closeRound">;
132
+ export type ExternalPreparedTurn = Pick<
133
+ PreparedRoleTurn,
134
+ "prompt" | "abortSignal" | "closeRound" | "recordSentInstruction"
135
+ >;
133
136
 
134
137
  export type ExternalHostRoundOutcome =
135
138
  | {
@@ -318,6 +321,9 @@ export async function driveExternalRoleTurnRounds(
318
321
  }
319
322
  }
320
323
  prompt = closure.retry.message;
324
+ // #1199: same-envelope 催交 / correctable retry — progress instruction is
325
+ // the bytes actually sent this round, not the first-mint summons.
326
+ prepared.recordSentInstruction?.(prompt);
321
327
  driver.afterRetry?.();
322
328
  }
323
329
 
@@ -264,7 +264,7 @@ export interface DurablePrincipalAuthority {
264
264
  type HostSessionManager = { getLeafEntry(): HostSessionEntry | undefined; getLeafId(): string | null | undefined; getEntries(): Iterable<HostSessionEntry>; getSessionDir(): string; getSessionFile(): string | undefined; getHeader?(): { readonly type: string; readonly id?: string } | null; setSessionFile?(path: string): void; appendCustomEntry?(customType: string, data?: unknown): unknown; };
265
265
 
266
266
  /** Context supplied by a host for one activation and its interceptable events. */
267
- export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; /** Public-invocation scope (#537); never process-global env. */ invocationScopeId?: string; /** Selected host axis (#537 / ADR 0082); never process-global env invent. */ host?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
267
+ export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; /** Public-invocation scope (#537); never process-global env. */ invocationScopeId?: string; /** Selected host axis (#537 / ADR 0082); never process-global env invent. */ host?: string; /** #1199 bytes actually sent to the seat this turn (progress instruction); input-seam / in-process only — never env/argv body. */ summonsInstruction?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
268
268
 
269
269
  /** Per-turn run directory; adapters must project any child-process identity. */
270
270
  export function runDirectoryFromHostContext(context: HostContext): string | undefined {
package/src/pi/adapter.ts CHANGED
@@ -45,44 +45,6 @@ export function createPiRoleRuntimeExtension(
45
45
  return (pi) => createRoleRuntimeExtension(dependencies)(createPiRoleHostAdapter(pi, options));
46
46
  }
47
47
 
48
- /** Project Pi's activation context onto the package-owned host contract. */
49
- function projectPiContext(context: ExtensionContext, transcriptFromContext?: (context: ExtensionContext) => string): HostContext {
50
- const sessionManager = context.sessionManager as SessionManager;
51
- const host: HostContext = {
52
- cwd: context.cwd,
53
- mode: context.mode,
54
- model: context.model === undefined ? undefined : { provider: context.model.provider },
55
- ...(typeof process.env.AK_ROLE_RUN_DIR === "string" && process.env.AK_ROLE_RUN_DIR.trim() !== ""
56
- ? { runDirectory: process.env.AK_ROLE_RUN_DIR }
57
- : {}),
58
- ...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
59
- ? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
60
- : {}),
61
- ...(typeof process.env.AK_ROLE_INVOCATION_SCOPE === "string" && process.env.AK_ROLE_INVOCATION_SCOPE.trim() !== ""
62
- ? { invocationScopeId: process.env.AK_ROLE_INVOCATION_SCOPE }
63
- : {}),
64
- ...(typeof process.env.AK_ROLE_HOST === "string" && process.env.AK_ROLE_HOST.trim() !== ""
65
- ? { host: process.env.AK_ROLE_HOST.trim() }
66
- : {}),
67
- sessionManager: {
68
- getLeafEntry: () => context.sessionManager.getLeafEntry(),
69
- getLeafId: () => context.sessionManager.getLeafId(),
70
- getEntries: () => context.sessionManager.getEntries(),
71
- getSessionDir: () => context.sessionManager.getSessionDir(),
72
- getSessionFile: () => context.sessionManager.getSessionFile(),
73
- getHeader: () => context.sessionManager.getHeader(),
74
- setSessionFile: (path) => sessionManager.setSessionFile(path),
75
- appendCustomEntry: (customType, data) => sessionManager.appendCustomEntry(customType, data),
76
- },
77
- ...(context.signal === undefined ? {} : { signal: context.signal }),
78
- ...(context.ui === undefined ? {} : { ui: { notify: (message, type) => context.ui.notify(message, type) } }),
79
- ...(transcriptFromContext === undefined ? {} : { transcript: () => transcriptFromContext(context) }),
80
- abort: () => context.abort(),
81
- };
82
- piContexts.set(host, context);
83
- return host;
84
- }
85
-
86
48
  /** Recover the Pi context only at the Pi composition boundary. */
87
49
  export function toPiContext(context: HostContext): ExtensionContext {
88
50
  const piContext = piContexts.get(context);
@@ -158,10 +120,63 @@ export function createPiRoleHostAdapter(
158
120
  let beforeAgentStartHandlers = 0;
159
121
  // Parameters of registered terminating tools — the schema Pi was handed.
160
122
  const registeredToolParameters = new Map<string, unknown>();
161
- // Decode transport exactly once before Pi's native handler chain. Pi retains
162
- // its own transform/image propagation and per-handler error isolation.
123
+ // Pi createContext() is fresh per emit/tool (runner.js). Do not cache HostContext by
124
+ // ExtensionContext identity. Share only this-turn 传召词 / 催交 on the adapter instance;
125
+ // model and identity pointers stay live on each fresh projection (#1199).
126
+ let turnSummonsInstruction: string | undefined;
127
+ const transcriptFromContext = options.transcriptFromContext;
128
+ const projectPiContext = (context: ExtensionContext): HostContext => {
129
+ const sessionManager = context.sessionManager as SessionManager;
130
+ const host: HostContext = {
131
+ cwd: context.cwd,
132
+ mode: context.mode,
133
+ model: context.model === undefined ? undefined : { provider: context.model.provider },
134
+ ...(typeof process.env.AK_ROLE_RUN_DIR === "string" && process.env.AK_ROLE_RUN_DIR.trim() !== ""
135
+ ? { runDirectory: process.env.AK_ROLE_RUN_DIR }
136
+ : {}),
137
+ ...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
138
+ ? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
139
+ : {}),
140
+ ...(typeof process.env.AK_ROLE_INVOCATION_SCOPE === "string" && process.env.AK_ROLE_INVOCATION_SCOPE.trim() !== ""
141
+ ? { invocationScopeId: process.env.AK_ROLE_INVOCATION_SCOPE }
142
+ : {}),
143
+ ...(typeof process.env.AK_ROLE_HOST === "string" && process.env.AK_ROLE_HOST.trim() !== ""
144
+ ? { host: process.env.AK_ROLE_HOST.trim() }
145
+ : {}),
146
+ sessionManager: {
147
+ getLeafEntry: () => context.sessionManager.getLeafEntry(),
148
+ getLeafId: () => context.sessionManager.getLeafId(),
149
+ getEntries: () => context.sessionManager.getEntries(),
150
+ getSessionDir: () => context.sessionManager.getSessionDir(),
151
+ getSessionFile: () => context.sessionManager.getSessionFile(),
152
+ getHeader: () => context.sessionManager.getHeader(),
153
+ setSessionFile: (path) => sessionManager.setSessionFile(path),
154
+ appendCustomEntry: (customType, data) => sessionManager.appendCustomEntry(customType, data),
155
+ },
156
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
157
+ ...(context.ui === undefined ? {} : { ui: { notify: (message, type) => context.ui.notify(message, type) } }),
158
+ ...(transcriptFromContext === undefined ? {} : { transcript: () => transcriptFromContext(context) }),
159
+ abort: () => context.abort(),
160
+ };
161
+ // exactOptionalPropertyTypes: wire via accessors so undefined stays "absent value"
162
+ // while every fresh projection shares the adapter-instance turn field.
163
+ Object.defineProperty(host, "summonsInstruction", {
164
+ enumerable: true,
165
+ configurable: true,
166
+ get: () => turnSummonsInstruction,
167
+ set: (value: string | undefined) => {
168
+ turnSummonsInstruction = value;
169
+ },
170
+ });
171
+ piContexts.set(host, context);
172
+ return host;
173
+ };
174
+ // Decode transport exactly once before Pi's native handler chain. Progress
175
+ // instruction is the model-facing body (what the seat actually receives) —
176
+ // no parallel 传召词 channel. Every input refreshes the turn field.
163
177
  pi.on("input", (value) => {
164
178
  const text = readUserDialogueStdin(value.text);
179
+ turnSummonsInstruction = text;
165
180
  return text === value.text ? { action: "continue" } : { action: "transform", text };
166
181
  });
167
182
  const host: RoleHost = {
@@ -172,10 +187,7 @@ export function createPiRoleHostAdapter(
172
187
  getFlag: (name) => pi.getFlag(name),
173
188
  registerTool: (tool) => {
174
189
  registeredToolParameters.set(tool.name, tool.parameters);
175
- return pi.registerTool(toPiToolDefinition(
176
- tool,
177
- (context) => projectPiContext(context, options.transcriptFromContext),
178
- ));
190
+ return pi.registerTool(toPiToolDefinition(tool, projectPiContext));
179
191
  },
180
192
  getAllTools: () => pi.getAllTools().map(({ name, sourceInfo }) => ({
181
193
  name,
@@ -184,7 +196,7 @@ export function createPiRoleHostAdapter(
184
196
  setActiveTools: (names) => pi.setActiveTools(names),
185
197
  getActiveTools: () => pi.getActiveTools(),
186
198
  on(...registration: HostEventRegistration) {
187
- const context = (value: ExtensionContext) => projectPiContext(value, options.transcriptFromContext);
199
+ const context = projectPiContext;
188
200
  if (registration[0] === "before_agent_start") {
189
201
  const [, handler] = registration;
190
202
  const position = beforeAgentStartHandlers;
@@ -438,7 +438,9 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
438
438
  config.extraPiArgs ?? [],
439
439
  );
440
440
  const args = buildExplicitInternalActivationArgs(roleEntry, extraArgs);
441
- const stdin = encodeUserDialogueStdin(piUserDialogueBody(turnRequest));
441
+ const transportBody = piUserDialogueBody(turnRequest);
442
+ // #1199: progress instruction is these same body bytes (actual seat input).
443
+ const stdin = encodeUserDialogueStdin(transportBody);
442
444
  // Shared envelope isolates this call's court identity: omitting courtAttemptId
443
445
  // must not inherit a parent process.env.AK_ROLE_COURT_ATTEMPT (#637).
444
446
  const env: NodeJS.ProcessEnv = {
@@ -458,6 +460,9 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
458
460
  // Selected host axis (#537 / ADR 0082): omit must not inherit a parent env value.
459
461
  if (request.host === undefined || request.host.trim() === "") delete env.AK_ROLE_HOST;
460
462
  else env.AK_ROLE_HOST = request.host.trim();
463
+ // #1199: 传召词 is unknown-length body — never argv/env (execve ARG_MAX / E2BIG).
464
+ // Typed stdin envelope carries dialogue + optional progress 传召词.
465
+ delete env.AK_ROLE_SUMMONS_INSTRUCTION;
461
466
  // #1132: the one effective delivery-request ceiling travels to the child
462
467
  // on its own env so the in-child role runtime and the worker submission
463
468
  // gate count with the same configured number the AK seam resolved. Omit
@@ -15,6 +15,12 @@ export type PreparedRoleTurn = Readonly<{
15
15
  systemPrompt: { readonly body: string; readonly materials: readonly unknown[] };
16
16
  /** Effective user prompt after host-side input transform (canonical Skill invocation). */
17
17
  prompt: string;
18
+ /**
19
+ * #1199: book the bytes actually sent this round as progress instruction
20
+ * (same-envelope 催交 / correctable retry). Production prepareRoleEnvelope
21
+ * always provides this; test stubs may omit.
22
+ */
23
+ recordSentInstruction?(text: string): void;
18
24
  /**
19
25
  * Host abort signal armed only by typed infrastructure failure (envelope
20
26
  * rememberInfrastructureFailure / non-correctable MCP catch). Lawful
@@ -585,7 +585,10 @@ export async function resolveReviewSeatDialogueBody(input: {
585
585
  return await loadLedgerPeerBody(sourceRunPath, input.projectRoot, input.home);
586
586
  }
587
587
 
588
- async function resolveInitialPrompt(
588
+ /**
589
+ * Initial host prompt for this turn (#1199: progress instruction = these bytes).
590
+ */
591
+ async function resolveInitialTurnPrompt(
589
592
  admitted: AdmittedRoleInvocation,
590
593
  env: InstructionSeatRunEnv,
591
594
  ): Promise<string> {
@@ -726,7 +729,7 @@ async function dispatchAdmitted(
726
729
  if (activeEnv.correlationId !== undefined && activeEnv.correlationId.trim() !== "") {
727
730
  await recordAdmittedCorrelation(admitted, activeEnv.correlationId);
728
731
  }
729
- const initialPromptText = await resolveInitialPrompt(admitted, activeEnv);
732
+ const initialPrompt = await resolveInitialTurnPrompt(admitted, activeEnv);
730
733
  const auto = "inCallAutoResume" in record && record.inCallAutoResume === true;
731
734
  if (auto) {
732
735
  // #1208: auto-resume resends this turn's actual content, never a filler phrase.
@@ -738,14 +741,14 @@ async function dispatchAdmitted(
738
741
  admitted,
739
742
  roleTurnOptions(activeEnv, admitted, {
740
743
  kind: "initial",
741
- prompt: initialPromptText,
744
+ prompt: initialPrompt,
742
745
  }),
743
746
  ),
744
747
  buildResumeRequest: () => buildInstructionSeatTurnRequest(
745
748
  admitted,
746
749
  roleTurnOptions(activeEnv, admitted, {
747
750
  kind: "resume",
748
- prompt: initialPromptText,
751
+ prompt: initialPrompt,
749
752
  }),
750
753
  ),
751
754
  adapters,
@@ -760,7 +763,7 @@ async function dispatchAdmitted(
760
763
  admitted,
761
764
  roleTurnOptions(activeEnv, admitted, {
762
765
  kind: "initial",
763
- prompt: initialPromptText,
766
+ prompt: initialPrompt,
764
767
  }),
765
768
  ),
766
769
  adapters,
@@ -899,7 +902,7 @@ function resumeSameParentInstructionSeat(input: {
899
902
  }
900
903
 
901
904
  /**
902
- * Same delivery rule as resolveInitialPrompt for same-parent resume summons.
905
+ * Same delivery rule as resolveInitialTurnPrompt for same-parent resume summons.
903
906
  * Shared resolver owns the input choice — callers only pass env fields.
904
907
  */
905
908
  async function sameParentDialogue(
@@ -1005,7 +1008,7 @@ async function runCountersignBody(
1005
1008
  // #1208 C1 / #1166 J11: one shared initial-prompt seam (incl. file flags).
1006
1009
  const turnProjection = roleTurnOptions(env, admitted, {
1007
1010
  kind: "initial",
1008
- prompt: await resolveInitialPrompt(admitted, env),
1011
+ prompt: await resolveInitialTurnPrompt(admitted, env),
1009
1012
  });
1010
1013
  const turnRequest = buildInstructionSeatTurnRequest(admitted, turnProjection);
1011
1014
  const result = await runPostAdmissionOneShot({
@@ -23,12 +23,14 @@ import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
23
23
  import {
24
24
  ensureRoleRunDirectory,
25
25
  ensureRoleRunPlacement,
26
- formatRunLeaf,
27
26
  isUnboundRunDirectory,
28
27
  listBookRunDirectories,
29
28
  roleRunPlacement,
29
+ sessionDirectoryOf,
30
+ sessionFileIn,
30
31
  type RoleRunSubject,
31
32
  } from "../role-run-placement.ts";
33
+ import { relocateTicketProgressForLeg } from "../ticket-progress.ts";
32
34
  import type {
33
35
  DurablePrincipal,
34
36
  DurablePrincipalAuthority,
@@ -580,15 +582,101 @@ export async function recordAdmittedCorrelation(
580
582
  mutable.correlationIds = correlationIds;
581
583
  }
582
584
 
585
+ /**
586
+ * #1199 R2: registered 起居郎 legs that already belong on this ticket.
587
+ * Unbound source → whole-leg rename + publish; already under ticket → publish
588
+ * leftover staging only. No scan of unregistered siblings.
589
+ */
590
+ async function relocateRegisteredChildDiarists(input: {
591
+ readonly parentRunDirectory: string;
592
+ readonly bookKey: string;
593
+ readonly ticketNumber: number;
594
+ readonly projectRoot: string;
595
+ readonly ledgerHome: string;
596
+ readonly authority: DurablePrincipalAuthority;
597
+ }): Promise<void> {
598
+ const childRunIds = readPageSync(input.parentRunDirectory, "admitted")?.childDiaristRunIds;
599
+ for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
600
+ if (typeof childRunId !== "string") continue;
601
+ const childTarget = roleRunPlacement(input.ledgerHome, {
602
+ bookKey: input.bookKey,
603
+ subject: { ticketNumber: input.ticketNumber },
604
+ runId: childRunId,
605
+ role: "diarist",
606
+ });
607
+ const unboundChildDirectory = roleRunPlacement(input.ledgerHome, {
608
+ bookKey: input.bookKey,
609
+ subject: { unbound: true },
610
+ runId: childRunId,
611
+ role: "diarist",
612
+ }).runDirectory;
613
+
614
+ if (existsSync(unboundChildDirectory)) {
615
+ input.authority.seal(childTarget);
616
+ await bindTicketNumberOnRunDirectory(unboundChildDirectory, input.ticketNumber);
617
+ await rehomeUnboundTicketProvenance(
618
+ unboundChildDirectory,
619
+ input.ticketNumber,
620
+ input.projectRoot,
621
+ homeFromRunDirectory(input.parentRunDirectory),
622
+ );
623
+ ensureRoleRunDirectory(input.ledgerHome, dirname(childTarget.runDirectory));
624
+ // #1199: whole-leg rename first; originals travel with the directory.
625
+ await rename(unboundChildDirectory, childTarget.runDirectory);
626
+ relocateTicketProgressForLeg({
627
+ runDirectory: childTarget.runDirectory,
628
+ });
629
+ // Finished child legs will not settle again; refresh derived host.original
630
+ // after the rename commit. A derived render fault must not undo or block
631
+ // the committed placement (#1161 L1) — note and keep the true cause.
632
+ try {
633
+ renderCurrentSync(childTarget.runDirectory);
634
+ } catch (error) {
635
+ process.stderr.write(
636
+ `[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`,
637
+ );
638
+ }
639
+ continue;
640
+ }
641
+
642
+ // Prior partial relocate left the child under the ticket with staging still held.
643
+ if (existsSync(childTarget.runDirectory)) {
644
+ relocateTicketProgressForLeg({
645
+ runDirectory: childTarget.runDirectory,
646
+ });
647
+ }
648
+ }
649
+ }
650
+
583
651
  /** File a role run under its latest typed ticket identity. */
584
652
  export async function relocateAdmittedRunToTicket(
585
653
  admitted: AdmittedRoleInvocation,
586
654
  authority: DurablePrincipalAuthority,
587
655
  heldLease?: { relocate(runDirectory: string): void },
588
656
  ): Promise<RunDirectoryRelocation | undefined> {
589
- if (admitted.ticketNumber === undefined || !isUnboundRunDirectory(admitted.runDirectory)) return undefined;
657
+ if (admitted.ticketNumber === undefined) return undefined;
658
+
659
+ const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(admitted.runDirectory));
660
+
661
+ // Already under the ticket: publish leftover progress.jsonl (self + registered children).
662
+ if (!isUnboundRunDirectory(admitted.runDirectory)) {
663
+ relocateTicketProgressForLeg({
664
+ runDirectory: admitted.runDirectory,
665
+ });
666
+ if (admitted.role !== "diarist") {
667
+ await relocateRegisteredChildDiarists({
668
+ parentRunDirectory: admitted.runDirectory,
669
+ bookKey: admitted.bookKey,
670
+ ticketNumber: admitted.ticketNumber,
671
+ projectRoot: admitted.projectRoot,
672
+ ledgerHome,
673
+ authority,
674
+ });
675
+ }
676
+ return undefined;
677
+ }
678
+
590
679
  const oldRunDirectory = admitted.runDirectory;
591
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
592
680
  const target = roleRunPlacement(ledgerHome, {
593
681
  bookKey: admitted.bookKey,
594
682
  subject: { ticketNumber: admitted.ticketNumber },
@@ -596,39 +684,18 @@ export async function relocateAdmittedRunToTicket(
596
684
  role: admitted.role,
597
685
  });
598
686
  ensureRoleRunDirectory(ledgerHome, dirname(target.runDirectory));
599
- // Host sealing is pure identity projection, but may reject the coordinates.
600
- // Keep that failure before the filesystem commit point.
601
- const principal = authority.seal(target);
687
+ // Pre-flight seal; final seal after the filesystem commit.
688
+ authority.seal(target);
602
689
 
603
690
  if (admitted.role !== "diarist") {
604
- const childRunIds = readPageSync(oldRunDirectory, "admitted")?.childDiaristRunIds;
605
- for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
606
- if (typeof childRunId !== "string") continue;
607
- const childDirectory = join(dirname(oldRunDirectory), formatRunLeaf(childRunId, "diarist"));
608
- // A child that already filed under a ticket keeps its own assertion.
609
- if (!existsSync(childDirectory)) continue;
610
- const childTarget = roleRunPlacement(ledgerHome, {
611
- bookKey: admitted.bookKey,
612
- subject: { ticketNumber: admitted.ticketNumber },
613
- runId: childRunId,
614
- role: "diarist",
615
- });
616
- authority.seal(childTarget);
617
- await bindTicketNumberOnRunDirectory(childDirectory, admitted.ticketNumber);
618
- await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
619
- ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
620
- await rename(childDirectory, childTarget.runDirectory);
621
- // Finished child legs will not settle again; refresh derived host.original
622
- // after the rename commit. A derived render fault must not undo or block
623
- // the committed placement (#1161 L1) — note and keep the true cause.
624
- try {
625
- renderCurrentSync(childTarget.runDirectory);
626
- } catch (error) {
627
- process.stderr.write(
628
- `[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`,
629
- );
630
- }
631
- }
691
+ await relocateRegisteredChildDiarists({
692
+ parentRunDirectory: oldRunDirectory,
693
+ bookKey: admitted.bookKey,
694
+ ticketNumber: admitted.ticketNumber,
695
+ projectRoot: admitted.projectRoot,
696
+ ledgerHome,
697
+ authority,
698
+ });
632
699
  }
633
700
 
634
701
  // The first ticket assignment moves only an unbound diarist's own diary.
@@ -637,21 +704,32 @@ export async function relocateAdmittedRunToTicket(
637
704
  }
638
705
 
639
706
  // Rename commits the run placement. Diary assignment above is a Sitian append
640
- // outside the atomic directory move.
641
- // Persisted paths are resolved from typed run identity on read.
707
+ // outside the atomic directory move. Session/receipts/progress travel with the leg.
642
708
  await rename(oldRunDirectory, target.runDirectory);
643
709
 
644
710
  // rename moved the open lock inode with the directory. Transfer cleanup and
645
- // admitted path ownership immediately after the commit — before any derived
646
- // render that may refuse (#1161 L1 / BASE order).
711
+ // live path ownership immediately after the commit — before final seal/render
712
+ // (#1161 L1 / BASE order). Live handles first so mid-turn setSessionFile sees
713
+ // the ticket leaf; final seal uses the post-move existence landing.
647
714
  heldLease?.relocate(target.runDirectory);
648
715
 
716
+ const actualSessionDirectory = sessionDirectoryOf(target.runDirectory);
717
+ const principal = authority.seal({
718
+ sessionDirectory: actualSessionDirectory,
719
+ sessionFile: sessionFileIn(actualSessionDirectory),
720
+ });
721
+
649
722
  rewriteAdmittedRoleRunPage(
650
723
  admitted as unknown as Record<string, unknown>,
651
724
  [{ oldRunDirectory, newRunDirectory: target.runDirectory }],
652
725
  );
653
726
  (admitted as { principal: DurablePrincipal }).principal = principal;
654
727
 
728
+ // After whole-leg rename + live-handle handoff: fill rounds and append ticket current.
729
+ relocateTicketProgressForLeg({
730
+ runDirectory: target.runDirectory,
731
+ });
732
+
655
733
  // current.json projects host.original from this run directory. Ownership is
656
734
  // already transferred; a refuse still carries the true cause to the caller
657
735
  // (#1161 L1) without leaving the leg under the unbound path.
@@ -835,6 +835,7 @@ function buildReceiptDeliveryRequest(input: {
835
835
  receiptDelivery: ReturnType<typeof createReceiptDeliveryPolicy>;
836
836
  }): RoleTurnRequest {
837
837
  const { request, receiptDelivery } = input;
838
+ // #1199: progress instruction is this turn's prompt (envelope books continuation.prompt).
838
839
  return {
839
840
  ...request,
840
841
  continuation: {
@@ -1696,6 +1697,7 @@ export function resumeTurnRequestProjectionOptions(
1696
1697
  const officerDialogue = isStationChildOfficerDialogue(admitted.role, env);
1697
1698
  const fileFlags = summonsPrepared?.attachments ?? [];
1698
1699
  let prompt: string;
1700
+ // #1199: progress instruction = this prompt (actual seat input, file flags included).
1699
1701
  if (request.message !== undefined) {
1700
1702
  if (summonsPrepared !== undefined) {
1701
1703
  // #755: same-ticket review / open-court — caller words + caller file flags.
@@ -1743,7 +1745,7 @@ export function resumeTurnRequestProjectionOptions(
1743
1745
  /** Shared new-turn and in-call auto-resume projection. Seat code supplies prompt and activation. */
1744
1746
  export function roleTurnOptions(
1745
1747
  env: PostAdmissionEnv,
1746
- admitted: { readonly correlationId?: string },
1748
+ admitted: { readonly correlationId?: string; readonly instruction?: string },
1747
1749
  continuation: RoleTurnRequest["continuation"],
1748
1750
  ): RoleTurnRequestProjectionOptions {
1749
1751
  const correlationId = env.correlationId ?? admitted.correlationId;
@@ -28,7 +28,7 @@ import type { NotarySourceRunLocator } from "../notary-contracts.ts";
28
28
  import {
29
29
  retainedRunPathsMatch,
30
30
  rewriteAdmittedRoleRunPage,
31
- rewriteRunDirectoryPathValue,
31
+ rewritePrincipalSessionPaths,
32
32
  } from "../role-run-relocation.ts";
33
33
  import type { PublicThinkingLevel } from "./registry.ts";
34
34
  import {
@@ -290,14 +290,16 @@ async function readRoleRunStateDisk(
290
290
  : runDirectory;
291
291
  const runDir = runDirectory;
292
292
  // Principal wire stays uninterpreted — authority owns legacy sessionFile fallback.
293
+ // #1199: leg-local session/ moves with the run-prefix rewrite.
294
+ const principalCoords = {
295
+ sessionDirectory: record.sessionDirectory,
296
+ ...(typeof record.sessionFile === "string" ? { sessionFile: record.sessionFile } : {}),
297
+ };
298
+ rewritePrincipalSessionPaths(principalCoords, storedRunDirectory, runDirectory);
293
299
  const principalWire: RoleRunPrincipalWire = {
294
- sessionDirectory: rewriteRunDirectoryPathValue(
295
- record.sessionDirectory,
296
- storedRunDirectory,
297
- runDirectory,
298
- ) as string,
299
- ...(typeof record.sessionFile === "string"
300
- ? { sessionFile: rewriteRunDirectoryPathValue(record.sessionFile, storedRunDirectory, runDirectory) as string }
300
+ sessionDirectory: principalCoords.sessionDirectory as string,
301
+ ...(typeof principalCoords.sessionFile === "string"
302
+ ? { sessionFile: principalCoords.sessionFile }
301
303
  : {}),
302
304
  };
303
305
  const phase =
@@ -131,6 +131,9 @@ export function projectRoleTurnRequest(
131
131
  ...(options.deliveryRequestLimit === undefined
132
132
  ? {}
133
133
  : { deliveryRequestLimit: options.deliveryRequestLimit }),
134
+ ...(options.host === undefined || options.host.trim() === ""
135
+ ? {}
136
+ : { host: options.host.trim() }),
134
137
  };
135
138
  }
136
139
 
@@ -141,6 +141,7 @@ export async function reportTicketFromHostContext(
141
141
  if (process.env.AK_ROLE_RUN_DIR === runDirectory) {
142
142
  process.env.AK_ROLE_RUN_DIR = nextDirectory;
143
143
  }
144
+ // Leg-local session/: rewrite the open handle under the renamed run prefix.
144
145
  const projectedSessionFile = typeof priorSessionFile === "string" && priorSessionFile.trim() !== ""
145
146
  ? rewriteRunDirectoryPathValue(priorSessionFile, runDirectory, nextDirectory)
146
147
  : sessionFileOf(nextDirectory);
@@ -148,9 +149,12 @@ export async function reportTicketFromHostContext(
148
149
  context.sessionManager.setSessionFile?.(projectedSessionFile);
149
150
  }
150
151
  };
151
- const relocation = alreadyPlaced
152
- ? undefined
153
- : await relocateAdmittedRunToTicket(admitted, authority, { relocate: applyLiveHandles });
152
+ // Always enter relocate: unbound→rename+publish; already-on-ticket→publish leftover staging.
153
+ const relocation = await relocateAdmittedRunToTicket(
154
+ admitted,
155
+ authority,
156
+ alreadyPlaced ? undefined : { relocate: applyLiveHandles },
157
+ );
154
158
  const nextDirectory = relocation?.newRunDirectory ?? admitted.runDirectory;
155
159
  if (relocation === undefined) applyLiveHandles(nextDirectory);
156
160
 
@@ -704,6 +704,8 @@ export async function prepareRoleEnvelope(options: {
704
704
  const record = value as Record<string, unknown>;
705
705
  if (record.action === "transform" && typeof record.text === "string") prompt = record.text;
706
706
  }
707
+ // #1199: progress instruction = bytes actually handed to the seat this turn.
708
+ context.summonsInstruction = prompt;
707
709
  // Book the user assignment so judge audit subjects recover it from parent books.
708
710
  if (typeof prompt === "string" && prompt.trim() !== "") {
709
711
  sessionEntries.push({
@@ -751,6 +753,9 @@ export async function prepareRoleEnvelope(options: {
751
753
  }],
752
754
  systemPrompt: { body: systemPromptBody, materials: readingMaterials },
753
755
  prompt,
756
+ recordSentInstruction(text: string) {
757
+ context.summonsInstruction = text;
758
+ },
754
759
  abortSignal: hostAbort.signal,
755
760
  closeRound,
756
761
  dispose,
@@ -34,12 +34,28 @@ export function parseRunLeaf(name: string): { readonly runId: string; readonly r
34
34
  return { runId: name.slice(0, at), role: name.slice(at + 1) };
35
35
  }
36
36
 
37
+ export const ROLE_RUN_SESSION_FILENAME = "session.jsonl";
38
+
39
+ /**
40
+ * Ticket or unbound subject directory owning a run under `<subject>/runs/<leaf>`.
41
+ * Used by ticket-progress relative paths — not a second session landing.
42
+ */
43
+ export function subjectDirectoryOfRun(runDirectory: string): string | undefined {
44
+ const runsParent = dirname(runDirectory);
45
+ if (basename(runsParent) !== "runs") return undefined;
46
+ const subjectDirectory = dirname(runsParent);
47
+ const subjectName = basename(subjectDirectory);
48
+ if (subjectName === "unbound" || /^[1-9]\d*$/.test(subjectName)) {
49
+ return subjectDirectory;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ /** #1199 / main: session originals stay under the leg — `<run>/session`. */
37
55
  export function sessionDirectoryOf(runDirectory: string): string {
38
56
  return join(runDirectory, "session");
39
57
  }
40
58
 
41
- export const ROLE_RUN_SESSION_FILENAME = "session.jsonl";
42
-
43
59
  export function sessionFileIn(sessionDirectory: string): string {
44
60
  return join(sessionDirectory, ROLE_RUN_SESSION_FILENAME);
45
61
  }
@@ -177,5 +193,6 @@ export function ensureRoleRunPlacement(
177
193
  ledgerHome: string,
178
194
  placement: RoleRunPlacement,
179
195
  ): void {
196
+ ensureRoleRunDirectory(ledgerHome, placement.runDirectory);
180
197
  ensureRoleRunDirectory(ledgerHome, placement.sessionDirectory);
181
198
  }
@@ -13,7 +13,7 @@ import { dirname, join } from "node:path";
13
13
  import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.ts";
14
14
  import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.ts";
15
15
  import { readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
16
- import { roleRunPlacement } from "./role-run-placement.ts";
16
+ import { roleRunPlacement, sessionDirectoryOf } from "./role-run-placement.ts";
17
17
  import {
18
18
  INVOCATION_PAGE_PATH_FIELDS,
19
19
  PRINCIPAL_SESSION_PATH_FIELDS,
@@ -267,7 +267,7 @@ async function rewriteNestedMachinePathPages(
267
267
  pagesDirectory: string,
268
268
  rewrites: readonly RunDirectoryPathRewrite[],
269
269
  ): Promise<void> {
270
- const sessionRoot = join(pagesDirectory, "session");
270
+ const sessionRoot = sessionDirectoryOf(pagesDirectory);
271
271
  async function walk(directory: string): Promise<void> {
272
272
  let entries;
273
273
  try {