@akagilnc/pi-workflow-roles 0.1.4021 → 0.1.4035

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.
@@ -24,6 +24,8 @@ export type SubmissionLedgerEvent =
24
24
  readonly toolCallId: string;
25
25
  readonly toolName: string;
26
26
  readonly sequence: number;
27
+ /** Seat identity — machine fact beside the payload (ADR 0042 / #881). */
28
+ readonly role?: TerminalRoleName;
27
29
  /** LLM tool-call params at call time (#836 原话). */
28
30
  readonly params?: unknown;
29
31
  }
@@ -79,9 +81,19 @@ function attemptIdentity(context: HostContext, runId: string): string {
79
81
 
80
82
  /** One recorded role submission as stored — original payload plus host identity. */
81
83
  export type RecordedSubmissionRow = {
82
- readonly role: TerminalRoleName;
83
- readonly kind: "accepted" | "audit-escalation";
84
+ /**
85
+ * Seat identity when known. Historical correctable/infrastructure rows may omit it;
86
+ * payloads still project (#881). Terminal acceptance kind still requires a role match.
87
+ */
88
+ readonly role?: TerminalRoleName;
89
+ /**
90
+ * Recording class on the ledger. Terminal acceptance kind still only follows
91
+ * `accepted` / `audit-escalation`; every class carries the original payload (#881).
92
+ */
93
+ readonly kind: "accepted" | "audit-escalation" | "correctable-rejection" | "infrastructure" | "candidate";
84
94
  readonly accepted: unknown;
95
+ /** Present when the ledger row names the tool call — used to dedupe candidate+outcome. */
96
+ readonly toolCallId?: string;
85
97
  };
86
98
 
87
99
  /** Closed-submission callback: original payload, no status/facts projection (#836). */
@@ -191,8 +203,59 @@ function recordedRole(payload: { role?: unknown; projection?: { role?: unknown }
191
203
  return undefined;
192
204
  }
193
205
 
206
+ /** Prefer a more complete row for the same tool call without inventing a sole winner across calls. */
207
+ function rowRank(kind: RecordedSubmissionRow["kind"]): number {
208
+ switch (kind) {
209
+ case "accepted":
210
+ return 4;
211
+ case "audit-escalation":
212
+ return 3;
213
+ case "correctable-rejection":
214
+ case "infrastructure":
215
+ return 2;
216
+ case "candidate":
217
+ return 1;
218
+ }
219
+ }
220
+
194
221
  /**
195
- * All recorded role submissions in ledger order (#836 multi-submit).
222
+ * Same-call identity for reader pairing only (#881 / #836).
223
+ * attemptId is already on the record (subject/payload); bare toolCallId alone
224
+ * collapses distinct court attempts that reused a host call id.
225
+ */
226
+ function submissionCallKey(attemptId: string | undefined, toolCallId: string): string {
227
+ return `${attemptId ?? ""}\0${toolCallId}`;
228
+ }
229
+
230
+ function rowFromPayload(
231
+ kind: RecordedSubmissionRow["kind"],
232
+ payload: {
233
+ role?: unknown;
234
+ projection?: { role?: unknown };
235
+ accepted?: unknown;
236
+ params?: unknown;
237
+ toolCallId?: unknown;
238
+ },
239
+ accepted: unknown,
240
+ roleFallback?: TerminalRoleName,
241
+ ): RecordedSubmissionRow {
242
+ const role = recordedRole(payload) ?? roleFallback;
243
+ return {
244
+ ...(role === undefined ? {} : { role }),
245
+ kind,
246
+ accepted,
247
+ ...(typeof payload.toolCallId === "string" && payload.toolCallId.length > 0
248
+ ? { toolCallId: payload.toolCallId }
249
+ : {}),
250
+ };
251
+ }
252
+
253
+ /**
254
+ * All recorded role submissions in ledger order (#836 multi-submit / #881).
255
+ * Projects every original payload — sealed, audit-escalation, correctable-rejection,
256
+ * infrastructure, and bare candidate — without outcome-class filtering.
257
+ * Same call (candidate + outcome both carrying params) appears once — keyed by
258
+ * recorded attemptId + toolCallId so distinct court attempts stay distinct (#881).
196
259
  * `accepted` is the original payload; never rebuilt from a status/facts envelope.
197
260
  */
198
261
  export async function readRecordedSubmissionRows(
@@ -204,27 +267,95 @@ export async function readRecordedSubmissionRows(
204
267
  const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
205
268
  const scoped = recordsForAttempt(owned, scope.attemptId);
206
269
  const out: RecordedSubmissionRow[] = [];
270
+ const indexByCall = new Map<string, number>();
271
+ // Recover seat identity for historical non-sealed rows that omitted role (#881).
272
+ const roleByCall = new Map<string, TerminalRoleName>();
273
+ for (const record of scoped) {
274
+ const payload = record.payload as {
275
+ toolCallId?: unknown;
276
+ role?: unknown;
277
+ projection?: { role?: unknown };
278
+ } | undefined;
279
+ if (typeof payload?.toolCallId !== "string" || payload.toolCallId.length === 0) continue;
280
+ const role = recordedRole(payload);
281
+ if (role !== undefined) {
282
+ roleByCall.set(submissionCallKey(recordAttemptId(record), payload.toolCallId), role);
283
+ }
284
+ }
285
+
286
+ const take = (row: RecordedSubmissionRow, callKey: string | undefined): void => {
287
+ const toolCallId = row.toolCallId;
288
+ if (toolCallId !== undefined && callKey !== undefined) {
289
+ const existingIndex = indexByCall.get(callKey);
290
+ if (existingIndex !== undefined) {
291
+ const existing = out[existingIndex]!;
292
+ if (rowRank(row.kind) >= rowRank(existing.kind)) {
293
+ out[existingIndex] = {
294
+ ...row,
295
+ toolCallId,
296
+ // Keep a previously recovered role when the upgraded row still omits it.
297
+ ...(row.role === undefined && existing.role !== undefined ? { role: existing.role } : {}),
298
+ };
299
+ } else if (existing.role === undefined && row.role !== undefined) {
300
+ out[existingIndex] = { ...existing, role: row.role };
301
+ }
302
+ return;
303
+ }
304
+ indexByCall.set(callKey, out.length);
305
+ }
306
+ out.push(row);
307
+ };
308
+
207
309
  for (const record of scoped) {
310
+ const attemptId = recordAttemptId(record);
311
+ if (record.kind === "candidate") {
312
+ const payload = record.payload as Partial<Extract<SubmissionLedgerEvent, { type: "candidate" }>> & {
313
+ projection?: { role?: unknown };
314
+ } | undefined;
315
+ if (payload?.type !== "candidate" || payload.params === undefined) continue;
316
+ const callKey =
317
+ typeof payload.toolCallId === "string"
318
+ ? submissionCallKey(attemptId, payload.toolCallId)
319
+ : undefined;
320
+ const fallback = callKey !== undefined ? roleByCall.get(callKey) : undefined;
321
+ take(rowFromPayload("candidate", payload, payload.params, fallback), callKey);
322
+ continue;
323
+ }
208
324
  if (record.kind === "sealed") {
209
325
  const payload = record.payload as Partial<Extract<SubmissionLedgerEvent, { type: "sealed" }>> & {
210
326
  projection?: { role?: unknown };
211
327
  } | undefined;
212
328
  if (payload?.type !== "sealed" || payload.accepted === undefined) continue;
213
- const role = recordedRole(payload);
214
- if (role === undefined) continue;
215
- out.push({ role, kind: "accepted", accepted: payload.accepted });
329
+ const callKey =
330
+ typeof payload.toolCallId === "string"
331
+ ? submissionCallKey(attemptId, payload.toolCallId)
332
+ : undefined;
333
+ const fallback = callKey !== undefined ? roleByCall.get(callKey) : undefined;
334
+ take(rowFromPayload("accepted", payload, payload.accepted, fallback), callKey);
216
335
  continue;
217
336
  }
218
337
  if (record.kind !== "outcome") continue;
219
338
  const payload = record.payload as Partial<Extract<SubmissionLedgerEvent, { type: "outcome" }>> & {
220
339
  projection?: { role?: unknown };
221
340
  } | undefined;
222
- if (payload?.type !== "outcome" || payload.outcome !== "audit-escalation" || payload.accepted === undefined) {
223
- continue;
224
- }
225
- const role = recordedRole(payload);
226
- if (role === undefined) continue;
227
- out.push({ role, kind: "audit-escalation", accepted: payload.accepted });
341
+ if (payload?.type !== "outcome" || payload.accepted === undefined) continue;
342
+ const outcome = payload.outcome;
343
+ const kind: RecordedSubmissionRow["kind"] |
344
+ undefined =
345
+ outcome === "audit-escalation"
346
+ ? "audit-escalation"
347
+ : outcome === "correctable-rejection"
348
+ ? "correctable-rejection"
349
+ : outcome === "infrastructure"
350
+ ? "infrastructure"
351
+ : undefined;
352
+ if (kind === undefined) continue;
353
+ const callKey =
354
+ typeof payload.toolCallId === "string"
355
+ ? submissionCallKey(attemptId, payload.toolCallId)
356
+ : undefined;
357
+ const fallback = callKey !== undefined ? roleByCall.get(callKey) : undefined;
358
+ take(rowFromPayload(kind, payload, payload.accepted, fallback), callKey);
228
359
  }
229
360
  return out;
230
361
  }
@@ -241,7 +372,7 @@ export async function readRecordedSubmissions(
241
372
  return (await readRecordedSubmissionRows(cwd, runId, homeOrScope)).map((row) => row.accepted);
242
373
  }
243
374
 
244
- /** True when the run has at least one recorded accepted or audit-escalation payload. */
375
+ /** True when the run has at least one recorded original payload (any outcome class). */
245
376
  export async function hasRecordedSubmission(
246
377
  cwd: string,
247
378
  runId: string,
@@ -364,6 +495,7 @@ export function createSubmissionLedgerHost(
364
495
  toolCallId,
365
496
  toolName: tool.name,
366
497
  sequence: ++state.sequence,
498
+ role,
367
499
  params,
368
500
  });
369
501
  let result: HostToolResult<unknown>;
@@ -389,6 +521,7 @@ export function createSubmissionLedgerHost(
389
521
  outcome: "correctable-rejection",
390
522
  code: "typed-bounce",
391
523
  diagnostic: error instanceof Error ? error.message : String(error),
524
+ role,
392
525
  accepted: params,
393
526
  });
394
527
  throw error;
@@ -399,6 +532,7 @@ export function createSubmissionLedgerHost(
399
532
  toolCallId,
400
533
  outcome: "infrastructure",
401
534
  diagnostic: error instanceof Error ? error.message : String(error),
535
+ role,
402
536
  accepted: params,
403
537
  });
404
538
  throw error;