@akagilnc/pi-workflow-roles 0.1.3412 → 0.1.3439

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 (67) hide show
  1. package/dist/archivist-record-entry.js +21 -12
  2. package/dist/archivist-record-topology.js +20 -0
  3. package/dist/auditor-dossier-tool.js +67 -76
  4. package/dist/canonical-json.js +66 -69
  5. package/dist/compliance-transport.js +99 -86
  6. package/dist/countersign-contracts.js +23 -25
  7. package/dist/doctor-contracts.js +169 -145
  8. package/dist/engine-detour-tool.js +83 -87
  9. package/dist/engine-detour.js +148 -183
  10. package/dist/evidence-child-executor.js +10 -1
  11. package/dist/exact-utf8.js +11 -10
  12. package/dist/git-object-id.js +10 -5
  13. package/dist/gleaner-left-contracts.js +33 -35
  14. package/dist/grok/production-host.js +229 -118
  15. package/dist/inspector-contracts.js +28 -31
  16. package/dist/institutional-resolution.js +101 -104
  17. package/dist/ledger-session-read.js +241 -0
  18. package/dist/merger-contracts.js +66 -56
  19. package/dist/notary-contracts.js +91 -100
  20. package/dist/open-tool-schema.js +32 -31
  21. package/dist/package-contracts/collector-output.js +56 -43
  22. package/dist/package-contracts/fixer-output.js +52 -50
  23. package/dist/package-contracts/fixer-packet.js +67 -64
  24. package/dist/package-contracts/gatekeeper-output.js +48 -56
  25. package/dist/package-contracts/judge-output.js +23 -20
  26. package/dist/package-contracts/navigator-output.js +35 -40
  27. package/dist/package-contracts/reviewer-output.js +94 -110
  28. package/dist/package-contracts/terminating-infrastructure.js +62 -90
  29. package/dist/package-contracts/worker-output.js +30 -11
  30. package/dist/package-resources/engine-material.js +58 -94
  31. package/dist/public-cli/config.js +420 -484
  32. package/dist/public-cli/main.js +1219 -524
  33. package/dist/public-cli/registry.js +107 -114
  34. package/dist/receipt-delivery-policy.js +74 -76
  35. package/dist/session-opening-materials.js +44 -47
  36. package/dist/sha256.js +5 -3
  37. package/dist/sitian-appender.js +204 -210
  38. package/dist/sitian-contracts.js +15 -19
  39. package/dist/sitian-facade.js +5 -10
  40. package/dist/sitian-reader.js +37 -46
  41. package/dist/stream-idle-guard.js +63 -71
  42. package/dist/ticket-seat-memory.js +244 -0
  43. package/package.json +1 -1
  44. package/scripts/build-package.mjs +41 -0
  45. package/souls/audit-law.md +4 -0
  46. package/souls/countersign.md +1 -1
  47. package/souls/gleaner-left.md +1 -1
  48. package/souls/inspector.md +1 -1
  49. package/souls/notary.md +1 -1
  50. package/souls/reviewer.md +1 -1
  51. package/src/analyst-gate-cycles-read.ts +152 -83
  52. package/src/analyst-ledger.ts +170 -43
  53. package/src/archivist-record-entry.ts +32 -11
  54. package/src/archivist-record-topology.ts +39 -0
  55. package/src/evidence-child-executor.ts +14 -1
  56. package/src/grok/production-host.ts +44 -9
  57. package/src/host-contracts.ts +18 -0
  58. package/src/host-transition-prior-native.ts +8 -1
  59. package/src/ledger-session-read.ts +56 -0
  60. package/src/pi/durable-principal.ts +6 -0
  61. package/src/public-cli/inspector-run.ts +21 -10
  62. package/src/public-cli/invocation.ts +4 -20
  63. package/src/public-cli/notary-run.ts +21 -4
  64. package/src/public-cli/post-admission.ts +333 -42
  65. package/src/public-cli/settlement.ts +266 -81
  66. package/src/session-opening-materials.ts +4 -2
  67. package/src/ticket-seat-memory.ts +454 -0
@@ -1,34 +1,31 @@
1
- /**
2
- * Public Inspector (察院) terminating receipt contracts.
3
- * Lawful explicit releases: pass | bounce | escalate.
4
- * Dual path: gate-dispatched and independently callable (#568 / ADR 0074).
5
- */
6
- export const INSPECTOR_OUTPUT_TOOL_NAME = "ak_inspector_output";
7
- export const INSPECTOR_ACCEPTED_TEXT = "察院回执已接受";
8
- export function validateRecordedInspectorOutput(value) {
9
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
10
- throw new Error("Inspector output has no execution discriminator");
11
- }
12
- let status;
13
- try {
14
- status = value.status;
15
- }
16
- catch {
17
- throw new Error("Inspector output has no execution discriminator");
18
- }
19
- if (status === "pass" || status === "bounce" || status === "escalate") {
20
- return value;
21
- }
1
+ const INSPECTOR_OUTPUT_TOOL_NAME = "ak_inspector_output";
2
+ const INSPECTOR_ACCEPTED_TEXT = "\u5BDF\u9662\u56DE\u6267\u5DF2\u63A5\u53D7";
3
+ function validateRecordedInspectorOutput(value) {
4
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
22
5
  throw new Error("Inspector output has no execution discriminator");
6
+ }
7
+ let status;
8
+ try {
9
+ status = value.status;
10
+ } catch {
11
+ throw new Error("Inspector output has no execution discriminator");
12
+ }
13
+ if (status === "pass" || status === "bounce" || status === "escalate") {
14
+ return value;
15
+ }
16
+ throw new Error("Inspector output has no execution discriminator");
23
17
  }
24
- /** Machine-facing facts from an accepted 察院 release. Findings retained as submitted. */
25
- export function inspectorDecisiveFacts(output) {
26
- const facts = { status: output.status };
27
- const findings = output.findings;
28
- if (findings !== undefined)
29
- facts.findings = findings;
30
- const reason = output.reason;
31
- if (reason !== undefined)
32
- facts.reason = reason;
33
- return facts;
18
+ function inspectorDecisiveFacts(output) {
19
+ const facts = { status: output.status };
20
+ const findings = output.findings;
21
+ if (findings !== void 0) facts.findings = findings;
22
+ const reason = output.reason;
23
+ if (reason !== void 0) facts.reason = reason;
24
+ return facts;
34
25
  }
26
+ export {
27
+ INSPECTOR_ACCEPTED_TEXT,
28
+ INSPECTOR_OUTPUT_TOOL_NAME,
29
+ inspectorDecisiveFacts,
30
+ validateRecordedInspectorOutput
31
+ };
@@ -1,115 +1,112 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { seatModelOnly } from "./public-cli/registry.js";
4
- export const INSTITUTIONAL_RESOLUTION_FILE = "institutional-resolution.json";
5
- export class InstitutionalResolutionError extends Error {
6
- reason;
7
- constructor(message, reason, options) {
8
- super(message, options);
9
- this.name = "InstitutionalResolutionError";
10
- this.reason = reason;
11
- }
4
+ const INSTITUTIONAL_RESOLUTION_FILE = "institutional-resolution.json";
5
+ class InstitutionalResolutionError extends Error {
6
+ reason;
7
+ constructor(message, reason, options) {
8
+ super(message, options);
9
+ this.name = "InstitutionalResolutionError";
10
+ this.reason = reason;
11
+ }
12
12
  }
13
13
  function cleanSelection(model) {
14
- if (model === undefined)
15
- return undefined;
16
- if (typeof model.provider !== "string" || typeof model.model !== "string")
17
- return undefined;
18
- return {
19
- provider: model.provider,
20
- model: model.model,
21
- ...(model.thinking === undefined ? {} : { thinking: model.thinking }),
22
- };
14
+ if (model === void 0) return void 0;
15
+ if (typeof model.provider !== "string" || typeof model.model !== "string") return void 0;
16
+ return {
17
+ provider: model.provider,
18
+ model: model.model,
19
+ ...model.thinking === void 0 ? {} : { thinking: model.thinking }
20
+ };
23
21
  }
24
- /**
25
- * Authority for configured province-officer model+source (#453/#620):
26
- * own persistent > gatekeeper persistent > unconfigured.
27
- * Shared by resolveInstitutionalSeatSelections (plus parent fallback),
28
- * resolveEffectiveSeat subordinate branch, and roles/config display.
29
- * Model-axis projection stays seatModelOnly (single implementation in registry).
30
- */
31
- export function resolveConfiguredProvinceOfficer(config, officer) {
32
- const ownModel = seatModelOnly(config.seats[officer]);
33
- if (ownModel !== undefined) {
34
- return { selection: ownModel, source: "persistent" };
35
- }
36
- if (officer === "gatekeeper") {
37
- return { source: "unconfigured" };
38
- }
39
- const gatekeeperModel = seatModelOnly(config.seats.gatekeeper);
40
- if (gatekeeperModel !== undefined) {
41
- return { selection: gatekeeperModel, source: "inherit-gatekeeper" };
42
- }
22
+ function resolveConfiguredProvinceOfficer(config, officer) {
23
+ const ownModel = seatModelOnly(config.seats[officer]);
24
+ if (ownModel !== void 0) {
25
+ return { selection: ownModel, source: "persistent" };
26
+ }
27
+ if (officer === "gatekeeper") {
43
28
  return { source: "unconfigured" };
29
+ }
30
+ const gatekeeperModel = seatModelOnly(config.seats.gatekeeper);
31
+ if (gatekeeperModel !== void 0) {
32
+ return { selection: gatekeeperModel, source: "inherit-gatekeeper" };
33
+ }
34
+ return { source: "unconfigured" };
44
35
  }
45
- /**
46
- * Resolve effective per-seat institutional selections (#518 §2 Hop 1; #620):
47
- * - gatekeeper / inspector / notary: resolveConfiguredProvinceOfficer then
48
- * parent effective on the province path only
49
- * - auditor / evidenceChild: parent effective
50
- * - navigator: explicit config seat only. Navigator model authority stays
51
- * `navigator-model.json`; the page never carries a parent-inherited navigator
52
- * seat, so host-neutral transport cannot shadow that setting (#590 Out of Scope:
53
- * 席位表/模型路由变更归 owner 域).
54
- */
55
- export function resolveInstitutionalSeatSelections(config, parentEffectiveModel) {
56
- const parentSelection = cleanSelection(parentEffectiveModel);
57
- const gatekeeper = resolveConfiguredProvinceOfficer(config, "gatekeeper").selection ?? parentSelection;
58
- const inspector = resolveConfiguredProvinceOfficer(config, "inspector").selection ?? parentSelection;
59
- const notary = resolveConfiguredProvinceOfficer(config, "notary").selection ?? parentSelection;
60
- const auditor = parentSelection;
61
- const evidenceChild = parentSelection;
62
- const navigator = cleanSelection(seatModelOnly(config.seats?.navigator));
63
- return {
64
- version: 1,
65
- seats: {
66
- ...(gatekeeper === undefined ? {} : { gatekeeper }),
67
- ...(inspector === undefined ? {} : { inspector }),
68
- ...(notary === undefined ? {} : { notary }),
69
- ...(auditor === undefined ? {} : { auditor }),
70
- ...(evidenceChild === undefined ? {} : { evidenceChild }),
71
- ...(navigator === undefined ? {} : { navigator }),
72
- },
73
- };
36
+ function resolveInstitutionalSeatSelections(config, parentEffectiveModel) {
37
+ const parentSelection = cleanSelection(parentEffectiveModel);
38
+ const gatekeeper = resolveConfiguredProvinceOfficer(config, "gatekeeper").selection ?? parentSelection;
39
+ const inspector = resolveConfiguredProvinceOfficer(config, "inspector").selection ?? parentSelection;
40
+ const notary = resolveConfiguredProvinceOfficer(config, "notary").selection ?? parentSelection;
41
+ const auditor = parentSelection;
42
+ const evidenceChild = parentSelection;
43
+ const navigator = cleanSelection(seatModelOnly(config.seats?.navigator));
44
+ return {
45
+ version: 1,
46
+ seats: {
47
+ ...gatekeeper === void 0 ? {} : { gatekeeper },
48
+ ...inspector === void 0 ? {} : { inspector },
49
+ ...notary === void 0 ? {} : { notary },
50
+ ...auditor === void 0 ? {} : { auditor },
51
+ ...evidenceChild === void 0 ? {} : { evidenceChild },
52
+ ...navigator === void 0 ? {} : { navigator }
53
+ }
54
+ };
74
55
  }
75
- /**
76
- * Persist institutional resolution page to run directory (#518 §2 Hop 2).
77
- * Rewritten on each dispatch and resume. Zero secrets inside page.
78
- */
79
- export async function writeInstitutionalResolutionPage(runDirectory, page) {
80
- const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
81
- await writeFile(filePath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
56
+ async function writeInstitutionalResolutionPage(runDirectory, page) {
57
+ const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
58
+ await writeFile(filePath, `${JSON.stringify(page, null, 2)}
59
+ `, "utf8");
82
60
  }
83
- /**
84
- * Read institutional seat selection from run directory (#518 §2 Hop 3).
85
- * Fails loud if page is missing, corrupted, or does not contain a resolution for the seat.
86
- */
87
- export async function readInstitutionalSeatSelection(runDirectory, seat) {
88
- const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
89
- let raw;
90
- try {
91
- raw = await readFile(filePath, "utf8");
92
- }
93
- catch (error) {
94
- throw new InstitutionalResolutionError(`institutional resolution page is missing at ${filePath}`, "missing-page", { cause: error });
95
- }
96
- let page;
97
- try {
98
- page = JSON.parse(raw);
99
- }
100
- catch (error) {
101
- throw new InstitutionalResolutionError(`institutional resolution page is corrupted at ${filePath}`, "corrupted", { cause: error });
102
- }
103
- if (typeof page !== "object" || page === null || page.version !== 1) {
104
- throw new InstitutionalResolutionError(`institutional resolution page format is invalid at ${filePath}`, "invalid-format");
105
- }
106
- const seats = page.seats;
107
- if (typeof seats !== "object" || seats === null) {
108
- throw new InstitutionalResolutionError(`institutional resolution page missing seats object at ${filePath}`, "invalid-format");
109
- }
110
- const selection = cleanSelection(seats[seat]);
111
- if (selection === undefined) {
112
- throw new InstitutionalResolutionError(`institutional resolution page has no resolution for seat "${seat}" at ${filePath}`, "missing-seat");
113
- }
114
- return selection;
61
+ async function readInstitutionalSeatSelection(runDirectory, seat) {
62
+ const filePath = join(runDirectory, INSTITUTIONAL_RESOLUTION_FILE);
63
+ let raw;
64
+ try {
65
+ raw = await readFile(filePath, "utf8");
66
+ } catch (error) {
67
+ throw new InstitutionalResolutionError(
68
+ `institutional resolution page is missing at ${filePath}`,
69
+ "missing-page",
70
+ { cause: error }
71
+ );
72
+ }
73
+ let page;
74
+ try {
75
+ page = JSON.parse(raw);
76
+ } catch (error) {
77
+ throw new InstitutionalResolutionError(
78
+ `institutional resolution page is corrupted at ${filePath}`,
79
+ "corrupted",
80
+ { cause: error }
81
+ );
82
+ }
83
+ if (typeof page !== "object" || page === null || page.version !== 1) {
84
+ throw new InstitutionalResolutionError(
85
+ `institutional resolution page format is invalid at ${filePath}`,
86
+ "invalid-format"
87
+ );
88
+ }
89
+ const seats = page.seats;
90
+ if (typeof seats !== "object" || seats === null) {
91
+ throw new InstitutionalResolutionError(
92
+ `institutional resolution page missing seats object at ${filePath}`,
93
+ "invalid-format"
94
+ );
95
+ }
96
+ const selection = cleanSelection(seats[seat]);
97
+ if (selection === void 0) {
98
+ throw new InstitutionalResolutionError(
99
+ `institutional resolution page has no resolution for seat "${seat}" at ${filePath}`,
100
+ "missing-seat"
101
+ );
102
+ }
103
+ return selection;
115
104
  }
105
+ export {
106
+ INSTITUTIONAL_RESOLUTION_FILE,
107
+ InstitutionalResolutionError,
108
+ readInstitutionalSeatSelection,
109
+ resolveConfiguredProvinceOfficer,
110
+ resolveInstitutionalSeatSelections,
111
+ writeInstitutionalResolutionPage
112
+ };
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Canonical ledger session JSONL read primitives (shared owner).
3
+ * Consumers (ticket-trajectory, analyst, …) must import here — no second parse kernel.
4
+ */
5
+ import { readFile } from "node:fs/promises";
6
+ /**
7
+ * Loud JSONL failure that still retains rows parsed before the bad line.
8
+ * Callers that only need the throw keep catching Error; owners that must
9
+ * surface partial typed facts (e.g. first-frame timestamp) read prefixRows.
10
+ */
11
+ export class LedgerSessionJsonlError extends Error {
12
+ path;
13
+ line;
14
+ prefixRows;
15
+ constructor(message, init) {
16
+ super(message);
17
+ this.name = "LedgerSessionJsonlError";
18
+ this.path = init.path;
19
+ this.line = init.line;
20
+ this.prefixRows = init.prefixRows;
21
+ }
22
+ }
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ /**
27
+ * Read session JSONL with honest live-tail semantics:
28
+ * a malformed line is tolerated only when it is an unfinished final
29
+ * fragment at EOF (no record terminator after it). Any malformed line
30
+ * completed by a line terminator must fail loudly with file and 1-based
31
+ * line context — even when no non-empty record follows — never silently
32
+ * under-count.
33
+ *
34
+ * Loud failures throw LedgerSessionJsonlError carrying prefixRows so the
35
+ * single parse kernel can still expose facts obtained before the bad line.
36
+ */
37
+ export async function readLedgerSessionJsonl(path) {
38
+ const text = await readFile(path, "utf8");
39
+ // split keeps a trailing empty segment iff text ends with "\n", so
40
+ // index < lines.length - 1 means this segment was terminated.
41
+ const lines = text.split("\n");
42
+ const rows = [];
43
+ for (let index = 0; index < lines.length; index += 1) {
44
+ const line = lines[index];
45
+ if (!line.trim())
46
+ continue;
47
+ let row;
48
+ try {
49
+ row = JSON.parse(line);
50
+ }
51
+ catch (error) {
52
+ if (!(error instanceof SyntaxError))
53
+ throw error;
54
+ const completedByTerminator = index < lines.length - 1;
55
+ if (completedByTerminator) {
56
+ throw new LedgerSessionJsonlError(`malformed JSONL record in ${path} at line ${index + 1}: ${error.message}`, { path, line: index + 1, prefixRows: rows });
57
+ }
58
+ // unfinished fragment at EOF — keep prior complete rows
59
+ break;
60
+ }
61
+ // Syntactically complete line: must be a session object. Silent omission
62
+ // would under-count ledger evidence (failure honesty).
63
+ if (!isRecord(row)) {
64
+ const kind = row === null ? "null" : Array.isArray(row) ? "array" : typeof row;
65
+ throw new LedgerSessionJsonlError(`complete non-object JSONL record in ${path} at line ${index + 1}: expected object, got ${kind}`, { path, line: index + 1, prefixRows: rows });
66
+ }
67
+ rows.push(row);
68
+ }
69
+ return rows;
70
+ }
71
+ /** First and last record timestamps in encounter order. */
72
+ export function extractSessionTimestampSpan(rows) {
73
+ let startedAt;
74
+ let endedAt;
75
+ for (const row of rows) {
76
+ if (typeof row.timestamp !== "string" || !row.timestamp)
77
+ continue;
78
+ if (startedAt === undefined)
79
+ startedAt = row.timestamp;
80
+ endedAt = row.timestamp;
81
+ }
82
+ return {
83
+ ...(startedAt !== undefined ? { startedAt } : {}),
84
+ ...(endedAt !== undefined ? { endedAt } : {}),
85
+ };
86
+ }
87
+ /**
88
+ * Ordered-unique model ids from session frames (first-seen order).
89
+ * Sources (same faces ticket-trajectory already reads — single parse kernel):
90
+ * - `model_change.modelId`
91
+ * - assistant `message.model`
92
+ * Blank / non-string values are skipped. Does not invent a default model.
93
+ */
94
+ export function extractSessionModelSequence(rows) {
95
+ const seen = new Set();
96
+ const ordered = [];
97
+ const push = (raw) => {
98
+ const model = raw.trim();
99
+ if (model === "" || seen.has(model))
100
+ return;
101
+ seen.add(model);
102
+ ordered.push(model);
103
+ };
104
+ for (const row of rows) {
105
+ if (row.type === "model_change" && typeof row.modelId === "string") {
106
+ push(row.modelId);
107
+ }
108
+ const message = isRecord(row.message) ? row.message : undefined;
109
+ if (message?.role === "assistant" && typeof message.model === "string") {
110
+ push(message.model);
111
+ }
112
+ }
113
+ return ordered;
114
+ }
115
+ /** First line of a bash `command` argument (sole owner of this summary). */
116
+ export function bashCommandFirstLine(command) {
117
+ const match = /^[^\r\n]*/.exec(command);
118
+ return match?.[0] ?? "";
119
+ }
120
+ /**
121
+ * Pair toolCall frames → toolResult frames by toolCallId.
122
+ * Throws when a tool-bearing frame is structurally unreadable for association
123
+ * (toolCall missing string id, toolResult missing string toolCallId).
124
+ * Unpaired open calls remain without endedAt — that is incomplete, not unreadable.
125
+ */
126
+ export function extractSessionToolIntervals(rows) {
127
+ const order = [];
128
+ const openById = new Map();
129
+ for (const row of rows) {
130
+ const rowTimestamp = typeof row.timestamp === "string" ? row.timestamp : undefined;
131
+ const message = isRecord(row.message) ? row.message : undefined;
132
+ if (message?.role === "assistant" && Array.isArray(message.content)) {
133
+ const callTimestamp = typeof message.timestamp === "string" && message.timestamp
134
+ ? message.timestamp
135
+ : rowTimestamp;
136
+ for (const part of message.content) {
137
+ if (!isRecord(part) || part.type !== "toolCall")
138
+ continue;
139
+ if (typeof part.id !== "string" || part.id.length === 0) {
140
+ throw new Error("toolCall frame missing string id");
141
+ }
142
+ if (typeof part.name !== "string" || part.name.length === 0) {
143
+ throw new Error(`toolCall ${part.id} missing string name`);
144
+ }
145
+ if (callTimestamp === undefined || callTimestamp.length === 0) {
146
+ throw new Error(`toolCall ${part.id} missing timestamp`);
147
+ }
148
+ if (openById.has(part.id)) {
149
+ throw new Error(`duplicate toolCall id ${part.id}`);
150
+ }
151
+ const args = isRecord(part.arguments) ? part.arguments : undefined;
152
+ // Ticket surface: only bash first-line summary is authorized here.
153
+ const command = part.name === "bash" &&
154
+ args !== undefined &&
155
+ typeof args.command === "string"
156
+ ? bashCommandFirstLine(args.command)
157
+ : undefined;
158
+ const interval = {
159
+ toolCallId: part.id,
160
+ toolName: part.name,
161
+ startedAt: callTimestamp,
162
+ ...(command !== undefined ? { command } : {}),
163
+ };
164
+ order.push(interval);
165
+ openById.set(part.id, interval);
166
+ }
167
+ }
168
+ if (message?.role === "toolResult") {
169
+ if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) {
170
+ throw new Error("toolResult frame missing string toolCallId");
171
+ }
172
+ const resultTimestamp = typeof message.timestamp === "string" && message.timestamp
173
+ ? message.timestamp
174
+ : rowTimestamp;
175
+ if (resultTimestamp === undefined || resultTimestamp.length === 0) {
176
+ throw new Error(`toolResult ${message.toolCallId} missing timestamp`);
177
+ }
178
+ const open = openById.get(message.toolCallId);
179
+ if (open === undefined) {
180
+ // Result without a prior call is still associable as a closed interval
181
+ // once a name is known; keep structural readability without inventing a call.
182
+ const toolName = typeof message.toolName === "string" && message.toolName.length > 0
183
+ ? message.toolName
184
+ : "unknown";
185
+ order.push({
186
+ toolCallId: message.toolCallId,
187
+ toolName,
188
+ startedAt: resultTimestamp,
189
+ endedAt: resultTimestamp,
190
+ });
191
+ continue;
192
+ }
193
+ if (open.endedAt !== undefined) {
194
+ throw new Error(`duplicate toolResult for toolCallId ${message.toolCallId}`);
195
+ }
196
+ open.endedAt = resultTimestamp;
197
+ }
198
+ }
199
+ return order.map((interval) => {
200
+ const base = {
201
+ toolCallId: interval.toolCallId,
202
+ toolName: interval.toolName,
203
+ startedAt: interval.startedAt,
204
+ ...(interval.command !== undefined ? { command: interval.command } : {}),
205
+ };
206
+ return interval.endedAt === undefined
207
+ ? base
208
+ : { ...base, endedAt: interval.endedAt };
209
+ });
210
+ }
211
+ export function intervalRowsAroundAnchor(rows, anchorIndex, isBindingRow) {
212
+ let start = 0;
213
+ for (let i = anchorIndex; i >= 0; i -= 1) {
214
+ if (isBindingRow(rows[i])) {
215
+ start = i;
216
+ break;
217
+ }
218
+ }
219
+ let end = rows.length;
220
+ for (let i = Math.max(anchorIndex, start) + 1; i < rows.length; i += 1) {
221
+ if (isBindingRow(rows[i])) {
222
+ end = i;
223
+ break;
224
+ }
225
+ }
226
+ return { rows: rows.slice(start, end), closed: end < rows.length };
227
+ }
228
+ /**
229
+ * Interval owned by the first binding row that satisfies `matches`.
230
+ * `undefined` when the volume carries no matching binding — callers must not
231
+ * silently treat the whole continuous volume as that owner.
232
+ */
233
+ export function intervalRowsForMatchingBinding(rows, isBindingRow, matches) {
234
+ for (let i = 0; i < rows.length; i += 1) {
235
+ const row = rows[i];
236
+ if (!isBindingRow(row) || !matches(row))
237
+ continue;
238
+ return intervalRowsAroundAnchor(rows, i, isBindingRow);
239
+ }
240
+ return undefined;
241
+ }