@pasko70/pibo 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runtime/routed-session.js +80 -19
- package/dist/agent-runtimes/codex-native/turn.js +27 -3
- package/dist/apps/chat/data/chat-data-mappers.js +8 -1
- package/dist/apps/chat/data/read-state-service.js +24 -3
- package/dist/apps/chat/message-command-dispatcher.js +16 -1
- package/dist/apps/chat/web-app.js +40 -19
- package/dist/apps/chat-ui/assets/{dist-BG0n7zLd.js → dist-BQ-TQZet.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D79vyxSX.js → dist-CYTRhePp.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D6TjFhAm.js → dist-Crj0LNXs.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DFZ8cwh0.js → dist-t24MVWWG.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-cOjokPrK.js → dist-tFj2S0WU.js} +1 -1
- package/dist/apps/chat-ui/assets/index-Bc9O0z52.css +1 -0
- package/dist/apps/chat-ui/assets/{index-RMHUTJ62.js → index-Cr2oFRhI.js} +77 -77
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/{index-xacbCyTx.js → index-DgKYVP-6.js} +11 -11
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli-session/localSessionSource.js +3 -2
- package/dist/core/output-render-sequence.js +63 -6
- package/dist/core/session-router.js +99 -11
- package/dist/data/async-chat-storage.js +7 -2
- package/dist/data/bounded-worker-client.js +1 -1
- package/dist/data/chat-read-projections.js +4 -4
- package/dist/data/chat-storage-worker.js +24 -3
- package/dist/data/ingest-service.js +73 -4
- package/dist/data/message-command-store.js +153 -11
- package/dist/data/schema.js +24 -3
- package/dist/data/storage-maintenance.js +344 -0
- package/dist/data/storage-verification-worker.js +25 -0
- package/dist/debug/cache.js +136 -0
- package/dist/debug/index.js +201 -1
- package/dist/debug/message-queue.js +108 -0
- package/dist/debug/output-collision-repair.js +140 -0
- package/dist/debug/output-integrity.js +38 -2
- package/dist/debug/output-repair.js +1 -0
- package/dist/debug/session.js +2 -0
- package/dist/debug/storage-backup.js +12 -4
- package/dist/debug/storage-maintenance.js +78 -0
- package/dist/debug/summary.js +1 -0
- package/dist/debug/trace.js +28 -0
- package/dist/gateway/cli.js +71 -7
- package/dist/gateway/server.js +1 -0
- package/dist/reliability/store.js +119 -23
- package/dist/session-ui/terminalRows.js +7 -8
- package/dist/sessions/pibo-data-store.js +18 -14
- package/dist/shared/cache-observability.js +73 -0
- package/dist/shared/model-inference-metrics.js +20 -7
- package/dist/shared/trace-event-projection.js +44 -6
- package/dist/web/channel.js +114 -12
- package/dist/web/http.js +105 -44
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-hEkrlRk-.css +0 -1
|
@@ -43,6 +43,7 @@ export function repairOutputTurn(input) {
|
|
|
43
43
|
actorId: "pibo-debug-repair",
|
|
44
44
|
event: current.terminalEvent,
|
|
45
45
|
createdAt,
|
|
46
|
+
persistenceProvenance: { producer: "debug-repair", projection: "product-history", phase: "operator-repair" },
|
|
46
47
|
});
|
|
47
48
|
const audit = data.eventLog.appendEvent({
|
|
48
49
|
sessionId: input.piboSessionId,
|
package/dist/debug/session.js
CHANGED
|
@@ -48,6 +48,7 @@ export function inspectDebugSession(input, stores, options = {}) {
|
|
|
48
48
|
`pibo debug messages ${parsed.piboSessionId} list`,
|
|
49
49
|
`pibo debug final ${parsed.piboSessionId}`,
|
|
50
50
|
`pibo debug trace ${parsed.piboSessionId} --check`,
|
|
51
|
+
`pibo debug cache ${parsed.piboSessionId}`,
|
|
51
52
|
`pibo debug failures ${parsed.piboSessionId}`,
|
|
52
53
|
`pibo debug events ${parsed.piboSessionId} --limit 20`,
|
|
53
54
|
];
|
|
@@ -114,6 +115,7 @@ export function inspectDebugSessionRuntime(input, stores) {
|
|
|
114
115
|
nextCommands: [
|
|
115
116
|
`pibo debug trace ${parsed.piboSessionId} --check`,
|
|
116
117
|
`pibo debug trace ${parsed.piboSessionId} --native-history --check`,
|
|
118
|
+
`pibo debug cache ${parsed.piboSessionId}`,
|
|
117
119
|
`pibo debug events ${parsed.piboSessionId} --limit 20`,
|
|
118
120
|
],
|
|
119
121
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { createStorageBackup, readStorageBackupManifest, verifyStorageBackup, restoreStorageBackup } from "../data/storage-backup.js";
|
|
3
|
+
import { recordStorageMaintenance } from "../data/storage-maintenance.js";
|
|
3
4
|
export async function runStorageBackupCli(args) {
|
|
4
5
|
const action = args[0];
|
|
5
6
|
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
@@ -19,12 +20,19 @@ No source deletion, vacuum, or overwrite of an existing restore destination.`);
|
|
|
19
20
|
const required = (name) => { const value = values[name]; if (typeof value !== "string" || !value)
|
|
20
21
|
throw Error(`Missing --${name}`); return value; };
|
|
21
22
|
let result;
|
|
22
|
-
if (action === "create")
|
|
23
|
-
|
|
23
|
+
if (action === "create") {
|
|
24
|
+
const source = required("source");
|
|
25
|
+
result = await createStorageBackup({ source, payloadRoot: required("payload-root"), destination: required("destination"), resume: values.resume, maxBytes: values["max-bytes"] ? Number(values["max-bytes"]) : undefined, maxPayloads: values["max-payloads"] ? Number(values["max-payloads"]) : undefined, maxMilliseconds: values["max-ms"] ? Number(values["max-ms"]) : undefined, maxWalGrowthBytes: values["max-wal-growth"] ? Number(values["max-wal-growth"]) : undefined });
|
|
26
|
+
recordStorageMaintenance(source, { operation: "backup", at: new Date().toISOString(), status: result.status, destination: required("destination") });
|
|
27
|
+
}
|
|
24
28
|
else if (action === "inspect")
|
|
25
29
|
result = await readStorageBackupManifest(required("archive"));
|
|
26
|
-
else if (action === "verify")
|
|
27
|
-
|
|
30
|
+
else if (action === "verify") {
|
|
31
|
+
const archive = required("archive");
|
|
32
|
+
result = await verifyStorageBackup(archive, AbortSignal.timeout(60000));
|
|
33
|
+
const manifest = await readStorageBackupManifest(archive);
|
|
34
|
+
recordStorageMaintenance(manifest.source, { operation: "backup", at: new Date().toISOString(), status: "verified", archive });
|
|
35
|
+
}
|
|
28
36
|
else if (action === "restore")
|
|
29
37
|
result = await restoreStorageBackup(required("archive"), required("destination"), AbortSignal.timeout(60000));
|
|
30
38
|
else
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { piboHomePath } from "../core/pibo-home.js";
|
|
3
|
+
import { checkpointStorage, inspectStorageStatus, maintainStorageRetention, verifyStorage } from "../data/storage-maintenance.js";
|
|
4
|
+
import { resolveDebugStore } from "./stores.js";
|
|
5
|
+
export async function runStorageMaintenanceCli(args) {
|
|
6
|
+
const action = args[0];
|
|
7
|
+
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
8
|
+
printHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const { values } = parseArgs({ args: args.slice(1), strict: true, options: {
|
|
12
|
+
store: { type: "string" }, path: { type: "string" }, json: { type: "boolean" },
|
|
13
|
+
"timeout-ms": { type: "string" }, full: { type: "boolean" }, quick: { type: "boolean" },
|
|
14
|
+
apply: { type: "boolean" }, "dry-run": { type: "boolean" }, mode: { type: "string" },
|
|
15
|
+
before: { type: "string" }, limit: { type: "string" }, "payload-root": { type: "string" },
|
|
16
|
+
"database-warn-bytes": { type: "string" }, "wal-warn-bytes": { type: "string" }, "payload-warn-bytes": { type: "string" },
|
|
17
|
+
} });
|
|
18
|
+
if (values.apply && values["dry-run"])
|
|
19
|
+
throw new Error("Choose either --dry-run or --apply");
|
|
20
|
+
const path = typeof values.path === "string" ? values.path : resolveStorePath(values.store);
|
|
21
|
+
let result;
|
|
22
|
+
if (action === "status" || action === "doctor") {
|
|
23
|
+
result = inspectStorageStatus({ path, databaseWarnBytes: numberValue(values["database-warn-bytes"]), walWarnBytes: numberValue(values["wal-warn-bytes"]), payloadWarnBytes: numberValue(values["payload-warn-bytes"]) });
|
|
24
|
+
}
|
|
25
|
+
else if (action === "verify") {
|
|
26
|
+
if (values.full && values.quick)
|
|
27
|
+
throw new Error("Choose either --quick or --full");
|
|
28
|
+
result = await verifyStorage({ path, mode: values.full ? "full" : "quick", timeoutMs: numberValue(values["timeout-ms"]) });
|
|
29
|
+
}
|
|
30
|
+
else if (action === "checkpoint") {
|
|
31
|
+
const mode = values.mode ?? "passive";
|
|
32
|
+
if (!new Set(["passive", "restart", "truncate"]).has(mode))
|
|
33
|
+
throw new Error("Checkpoint --mode must be passive, restart, or truncate");
|
|
34
|
+
result = checkpointStorage({ path, mode: mode, apply: values.apply });
|
|
35
|
+
}
|
|
36
|
+
else if (action === "retention") {
|
|
37
|
+
if (typeof values.before !== "string")
|
|
38
|
+
throw new Error("Storage retention requires --before <iso-date>");
|
|
39
|
+
result = await maintainStorageRetention({ path, before: values.before, limit: numberValue(values.limit), apply: values.apply, payloadRoot: typeof values["payload-root"] === "string" ? values["payload-root"] : piboHomePath("payloads") });
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
throw new Error(`Unknown storage action "${action}"; run pibo debug storage --help`);
|
|
43
|
+
if (values.json)
|
|
44
|
+
console.log(JSON.stringify(result, null, 2));
|
|
45
|
+
else
|
|
46
|
+
console.log(formatText(result));
|
|
47
|
+
}
|
|
48
|
+
function resolveStorePath(value) {
|
|
49
|
+
if (!value || value === "pibo-data")
|
|
50
|
+
return resolveDebugStore("pibo-data").path;
|
|
51
|
+
if (value === "reliability")
|
|
52
|
+
return resolveDebugStore("reliability").path;
|
|
53
|
+
throw new Error("--store must be pibo-data or reliability; use --path for another SQLite store");
|
|
54
|
+
}
|
|
55
|
+
function numberValue(value) { if (value === undefined)
|
|
56
|
+
return undefined; const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
57
|
+
throw new Error(`Invalid numeric value "${value}"`); return parsed; }
|
|
58
|
+
function formatText(value) { const object = value; const lines = [`pibo debug storage ${String(object.resultType ?? "result").split(".").at(-1)}`]; for (const [key, item] of Object.entries(object)) {
|
|
59
|
+
if (key === "resultType" || typeof item === "object")
|
|
60
|
+
continue;
|
|
61
|
+
lines.push(`${key}\t${String(item)}`);
|
|
62
|
+
} if (Array.isArray(object.warnings))
|
|
63
|
+
lines.push(...object.warnings.map((warning) => `warning\t${String(warning)}`)); lines.push("", "Use --json for bounded row, size, projection, and progress detail."); return lines.join("\n"); }
|
|
64
|
+
function printHelp() {
|
|
65
|
+
console.log(`pibo debug storage - bounded SQLite health and maintenance
|
|
66
|
+
|
|
67
|
+
Commands:
|
|
68
|
+
status [--store pibo-data|reliability | --path <sqlite>] [--json]
|
|
69
|
+
doctor [--store pibo-data|reliability | --path <sqlite>] [--json]
|
|
70
|
+
verify [--quick|--full] [--timeout-ms <n>] [--store <name>|--path <sqlite>] [--json]
|
|
71
|
+
checkpoint [--mode passive|restart|truncate] [--dry-run|--apply] [--store <name>|--path <sqlite>] [--json]
|
|
72
|
+
retention --before <iso-date> [--limit <1..10000>] [--dry-run|--apply] [--payload-root <dir>] [--store <name>|--path <sqlite>] [--json]
|
|
73
|
+
|
|
74
|
+
Status is read-only and reports DB/WAL/SHM/payload size, pages/freelist, bounded row counts, payload-reference integrity, maintenance metadata, and degraded thresholds.
|
|
75
|
+
Verification runs in a cancellable worker. Timeout is partial and never healthy. Quick/full checks can still be I/O intensive; use backup verification or offline checks when the online budget is insufficient.
|
|
76
|
+
Checkpoint and retention default to dry-run. Apply is bounded and audited in the database maintenance sidecar. Retention deletes only eligible live_delta rows; chat messages, audit events, idempotency evidence, and referenced payloads are preserved.
|
|
77
|
+
`);
|
|
78
|
+
}
|
package/dist/debug/summary.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function inspectDebugSummary(input, stores) {
|
|
|
42
42
|
`pibo debug failures ${parsed.piboSessionId}`,
|
|
43
43
|
`pibo debug messages ${parsed.piboSessionId} list`,
|
|
44
44
|
`pibo debug trace ${parsed.piboSessionId} --check`,
|
|
45
|
+
`pibo debug cache ${parsed.piboSessionId}`,
|
|
45
46
|
`pibo debug events ${parsed.piboSessionId} --limit 20`,
|
|
46
47
|
],
|
|
47
48
|
};
|
package/dist/debug/trace.js
CHANGED
|
@@ -7,6 +7,8 @@ import { compareTraceNodes } from "../shared/trace-nodes.js";
|
|
|
7
7
|
import { openReadOnlyDebugDatabase, withStorePath } from "./sql.js";
|
|
8
8
|
import { formatNextCommands } from "./next-commands.js";
|
|
9
9
|
import { resolveDebugTraceSessionStatus, summarizeDebugTraceStatus } from "./trace-status.js";
|
|
10
|
+
import { cacheUsageWarningText } from "../shared/cache-observability.js";
|
|
11
|
+
import { modelInferenceCacheReadRatio, modelInferenceCachedInputTokens, modelInferenceInputTokens, modelInferenceUncachedInputTokens, } from "../shared/model-inference-metrics.js";
|
|
10
12
|
export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
11
13
|
if (!stores.sessions.exists)
|
|
12
14
|
throw new Error(`Debug store "sessions" not found at ${stores.sessions.path}`);
|
|
@@ -113,6 +115,23 @@ export async function inspectDebugTraceNode(piboSessionId, stores, nodeId) {
|
|
|
113
115
|
nextCommands: node ? buildNodeNextCommands(piboSessionId, node) : [`pibo debug trace ${piboSessionId}`],
|
|
114
116
|
};
|
|
115
117
|
}
|
|
118
|
+
function formatMetric(value) {
|
|
119
|
+
return value === undefined ? "unknown" : String(value);
|
|
120
|
+
}
|
|
121
|
+
function formatRatio(value) {
|
|
122
|
+
return value === undefined ? "unknown" : `${(value * 100).toFixed(1)}%`;
|
|
123
|
+
}
|
|
124
|
+
function formatModelInference(record) {
|
|
125
|
+
return [
|
|
126
|
+
record.id,
|
|
127
|
+
`input=${formatMetric(modelInferenceInputTokens(record.metrics))}`,
|
|
128
|
+
`cacheRead=${formatMetric(modelInferenceCachedInputTokens(record.metrics))}`,
|
|
129
|
+
`uncached=${formatMetric(modelInferenceUncachedInputTokens(record.metrics))}`,
|
|
130
|
+
`cacheWrite=${formatMetric(record.metrics.cacheWriteTokens)}`,
|
|
131
|
+
`output=${formatMetric(record.metrics.outputTokens)}`,
|
|
132
|
+
`cacheReadRatio=${formatRatio(modelInferenceCacheReadRatio(record.metrics))}`,
|
|
133
|
+
].join("\t");
|
|
134
|
+
}
|
|
116
135
|
export function formatDebugTrace(result, options = {}) {
|
|
117
136
|
const lines = [
|
|
118
137
|
`piboSessionId: ${result.piboSessionId}`,
|
|
@@ -152,6 +171,12 @@ export function formatDebugTrace(result, options = {}) {
|
|
|
152
171
|
order: node.order,
|
|
153
172
|
};
|
|
154
173
|
lines.push(columns.map((column) => values[column] ?? "").join("\t"));
|
|
174
|
+
for (const inference of node.modelInferences ?? []) {
|
|
175
|
+
lines.push(`model-inference\t${formatModelInference(inference)}`);
|
|
176
|
+
const warning = inference.cacheObservation && cacheUsageWarningText(inference.cacheObservation);
|
|
177
|
+
if (warning)
|
|
178
|
+
lines.push(`cache-warning\t${inference.id}\t${warning}`);
|
|
179
|
+
}
|
|
155
180
|
}
|
|
156
181
|
lines.push(`nodes: ${result.nodes.length}${result.nodes.length !== result.rawNodeCount ? ` of ${result.rawNodeCount}` : ""}`);
|
|
157
182
|
if (result.checks) {
|
|
@@ -194,6 +219,8 @@ export function formatDebugTraceNode(result) {
|
|
|
194
219
|
lines.push(`runId: ${node.runId}`);
|
|
195
220
|
if (node.toolCallId)
|
|
196
221
|
lines.push(`toolCallId: ${node.toolCallId}`);
|
|
222
|
+
for (const inference of node.modelInferences ?? [])
|
|
223
|
+
lines.push(`modelInference: ${JSON.stringify(inference)}`);
|
|
197
224
|
lines.push(...formatNextCommands(result.nextCommands));
|
|
198
225
|
return lines.join("\n");
|
|
199
226
|
}
|
|
@@ -214,6 +241,7 @@ function flattenTraceNodes(nodes, depth = 0) {
|
|
|
214
241
|
startedAt: node.startedAt,
|
|
215
242
|
completedAt: node.completedAt,
|
|
216
243
|
childrenCount: node.children.length,
|
|
244
|
+
...(node.modelInferences?.length ? { modelInferences: node.modelInferences } : {}),
|
|
217
245
|
depth,
|
|
218
246
|
},
|
|
219
247
|
...flattenTraceNodes(node.children, depth + 1),
|
package/dist/gateway/cli.js
CHANGED
|
@@ -231,14 +231,40 @@ function activeRun(value) {
|
|
|
231
231
|
piboSessionId: stringValue(obj.controllerPiboSessionId) ?? stringValue(obj.piboSessionId),
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
+
function runJobReliability(value) {
|
|
235
|
+
const obj = objectValue(value);
|
|
236
|
+
if (!obj || (obj.status !== "ok" && obj.status !== "degraded")
|
|
237
|
+
|| !Number.isInteger(obj.expiredOrphanRunJobs) || Number(obj.expiredOrphanRunJobs) < 0
|
|
238
|
+
|| !Number.isInteger(obj.orphanRunDeadLetters) || Number(obj.orphanRunDeadLetters) < 0)
|
|
239
|
+
return undefined;
|
|
240
|
+
return {
|
|
241
|
+
status: obj.status,
|
|
242
|
+
expiredOrphanRunJobs: Number(obj.expiredOrphanRunJobs),
|
|
243
|
+
orphanRunDeadLetters: Number(obj.orphanRunDeadLetters),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function durableMessageQueueStatus(value) {
|
|
247
|
+
const obj = objectValue(value);
|
|
248
|
+
if (!obj)
|
|
249
|
+
return undefined;
|
|
250
|
+
const status = obj.status === "healthy" || obj.status === "degraded" || obj.status === "ambiguous" ? obj.status : undefined;
|
|
251
|
+
const storage = objectValue(obj.storage);
|
|
252
|
+
const counts = Array.isArray(obj.counts) ? obj.counts.flatMap(value => { const row = objectValue(value); return row ? [{ state: stringValue(row.state), delivery: stringValue(row.delivery), count: numberValue(row.count), bytes: numberValue(row.bytes) }] : []; }) : undefined;
|
|
253
|
+
const affectedScopes = Array.isArray(obj.affectedScopes) ? obj.affectedScopes.flatMap(value => { const row = objectValue(value); return row ? [{ sessionId: stringValue(row.sessionId), roomId: stringValue(row.roomId), blockingCommandId: stringValue(row.blockingCommandId), blockedSince: numberValue(row.blockedSince), blockedSuccessors: numberValue(row.blockedSuccessors) }] : []; }) : undefined;
|
|
254
|
+
return { status, storage: storage ? { available: booleanValue(storage.available), error: stringValue(storage.error) } : undefined, counts, interruptedPredecessors: numberValue(obj.interruptedPredecessors), blockedSuccessors: numberValue(obj.blockedSuccessors), expiredOwnedLeases: numberValue(obj.expiredOwnedLeases), oldestDispatchableWaitMs: numberValue(obj.oldestDispatchableWaitMs), oldestBlockedWaitMs: numberValue(obj.oldestBlockedWaitMs), affectedScopes, degradedReasons: Array.isArray(obj.degradedReasons) && obj.degradedReasons.every(v => typeof v === "string") ? obj.degradedReasons : undefined, admissionCapacity: obj.admissionCapacity, bounded: obj.bounded };
|
|
255
|
+
}
|
|
234
256
|
function parseGatewaySafetyPayload(payload, reachable) {
|
|
235
257
|
const obj = objectValue(payload);
|
|
236
258
|
const mode = obj && (obj.mode === "dev" || obj.mode === "prod" || obj.mode === "fallback") ? obj.mode : "unknown";
|
|
237
259
|
const runtimeStatuses = Array.isArray(obj?.runtimeStatuses) ? obj.runtimeStatuses.map(runtimeStatus).filter((item) => Boolean(item)) : [];
|
|
238
260
|
const activeRuns = Array.isArray(obj?.activeRuns) ? obj.activeRuns.map(activeRun).filter((item) => Boolean(item)) : [];
|
|
261
|
+
const reliability = obj?.reliability === undefined ? undefined : runJobReliability(obj.reliability);
|
|
262
|
+
const durableMessageQueue = durableMessageQueueStatus(obj?.durableMessageQueue);
|
|
239
263
|
const incomplete = !Array.isArray(obj?.runtimeStatuses) || !Array.isArray(obj?.activeRuns)
|
|
240
|
-
|| runtimeStatuses.length !== obj.runtimeStatuses.length || activeRuns.length !== obj.activeRuns.length
|
|
241
|
-
|
|
264
|
+
|| runtimeStatuses.length !== obj.runtimeStatuses.length || activeRuns.length !== obj.activeRuns.length
|
|
265
|
+
|| (obj?.reliability !== undefined && !reliability)
|
|
266
|
+
|| (obj?.durableMessageQueue !== undefined && !durableMessageQueue?.status);
|
|
267
|
+
return { reachable, mode, generation: stringValue(obj?.generation), health: obj?.health, runtimeStatuses, activeRuns, reliability, durableMessageQueue, ambiguous: incomplete || booleanValue(obj?.ambiguous) };
|
|
242
268
|
}
|
|
243
269
|
export function checkActiveWork(status, target = "web") {
|
|
244
270
|
const reasons = [];
|
|
@@ -337,7 +363,8 @@ function printSafetyStatus(target, status) {
|
|
|
337
363
|
console.log(` mode: ${status.mode}`);
|
|
338
364
|
if (status.error)
|
|
339
365
|
console.log(` status error: ${status.error}`);
|
|
340
|
-
console.log(
|
|
366
|
+
console.log(" runtime queue layer:");
|
|
367
|
+
console.log(` sessions: ${status.runtimeStatuses.length}`);
|
|
341
368
|
for (const session of status.runtimeStatuses) {
|
|
342
369
|
console.log(` ${session.piboSessionId ?? "unknown"}: processing=${session.processing === true} streaming=${session.streaming === true} queued=${session.queuedMessages ?? 0}`);
|
|
343
370
|
if (session.activeEventId)
|
|
@@ -359,6 +386,31 @@ function printSafetyStatus(target, status) {
|
|
|
359
386
|
console.log(` active yielded runs: ${status.activeRuns.length}`);
|
|
360
387
|
for (const run of status.activeRuns)
|
|
361
388
|
console.log(` ${run.runId ?? "unknown"}: ${run.status ?? "active"}${run.toolName ? ` (${run.toolName})` : ""} session=${run.piboSessionId ?? "unknown"}`);
|
|
389
|
+
if (status.reliability) {
|
|
390
|
+
console.log(` run-job reliability: ${status.reliability.status}`);
|
|
391
|
+
console.log(` expired orphan jobs: ${status.reliability.expiredOrphanRunJobs}`);
|
|
392
|
+
console.log(` orphan DLQ records: ${status.reliability.orphanRunDeadLetters}`);
|
|
393
|
+
}
|
|
394
|
+
console.log(" durable message queue layer:");
|
|
395
|
+
const durable = status.durableMessageQueue;
|
|
396
|
+
if (!durable)
|
|
397
|
+
console.log(" status: unavailable (gateway app did not report this layer)");
|
|
398
|
+
else {
|
|
399
|
+
console.log(` status: ${durable.status ?? "ambiguous"}`);
|
|
400
|
+
console.log(` storage: ${durable.storage?.available === true ? "available" : "unavailable"}${durable.storage?.error ? ` (${durable.storage.error})` : ""}`);
|
|
401
|
+
for (const row of durable.counts ?? [])
|
|
402
|
+
console.log(` ${row.delivery ?? "unknown"}.${row.state ?? "unknown"}: count=${row.count ?? 0} bytes=${row.bytes ?? 0}`);
|
|
403
|
+
console.log(` interrupted predecessors: ${durable.interruptedPredecessors ?? 0}`);
|
|
404
|
+
console.log(` FIFO-blocked successors: ${durable.blockedSuccessors ?? 0}`);
|
|
405
|
+
console.log(` expired owned leases: ${durable.expiredOwnedLeases ?? 0}`);
|
|
406
|
+
console.log(` oldest dispatchable wait: ${durable.oldestDispatchableWaitMs ?? 0}ms`);
|
|
407
|
+
console.log(` oldest blocked wait: ${durable.oldestBlockedWaitMs ?? 0}ms`);
|
|
408
|
+
for (const scope of durable.affectedScopes ?? [])
|
|
409
|
+
console.log(` affected session=${scope.sessionId ?? "unknown"} room=${scope.roomId ?? "unknown"} blocker=${scope.blockingCommandId ?? "unknown"} successors=${scope.blockedSuccessors ?? 0}`);
|
|
410
|
+
for (const reason of durable.degradedReasons ?? [])
|
|
411
|
+
console.log(` degraded: ${reason}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(" next: pibo debug message-queue");
|
|
362
414
|
}
|
|
363
415
|
function managerRequiresShell(command) {
|
|
364
416
|
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
@@ -385,8 +437,11 @@ async function waitForManagedGatewayHealth(target) {
|
|
|
385
437
|
async function runManagedGatewayCommand(target, command, args, argv = process.argv) {
|
|
386
438
|
if (command === "status" || command === "doctor") {
|
|
387
439
|
const status = await readGatewaySafetyStatus(target);
|
|
388
|
-
|
|
389
|
-
|
|
440
|
+
if (args.includes("--json"))
|
|
441
|
+
console.log(JSON.stringify({ ...status, nextCommands: [`pibo gateway ${target} doctor`, `pibo debug message-queue`] }, null, 2));
|
|
442
|
+
else
|
|
443
|
+
printSafetyStatus(target, status);
|
|
444
|
+
if (target === "web" && !args.includes("--json")) {
|
|
390
445
|
const active = checkActiveWork(status, target);
|
|
391
446
|
if (active.unsafe) {
|
|
392
447
|
console.log(" restart safety: blocked");
|
|
@@ -397,8 +452,16 @@ async function runManagedGatewayCommand(target, command, args, argv = process.ar
|
|
|
397
452
|
console.log(" restart safety: idle");
|
|
398
453
|
printRestartApproval(status);
|
|
399
454
|
}
|
|
400
|
-
if (command === "doctor")
|
|
401
|
-
process.exitCode = status.reachable
|
|
455
|
+
if (command === "doctor") {
|
|
456
|
+
process.exitCode = status.reachable
|
|
457
|
+
&& !status.error
|
|
458
|
+
&& status.mode === expectedMode(target)
|
|
459
|
+
&& status.reliability?.status !== "degraded"
|
|
460
|
+
&& status.durableMessageQueue?.status === "healthy"
|
|
461
|
+
&& status.durableMessageQueue.storage?.available === true
|
|
462
|
+
? 0
|
|
463
|
+
: 1;
|
|
464
|
+
}
|
|
402
465
|
return true;
|
|
403
466
|
}
|
|
404
467
|
if (command === "start") {
|
|
@@ -730,6 +793,7 @@ Commands:
|
|
|
730
793
|
dev doctor Check dev gateway health
|
|
731
794
|
|
|
732
795
|
Options:
|
|
796
|
+
--json Print status or doctor output as JSON with next discovery commands
|
|
733
797
|
--force --confirm <snapshot-token>
|
|
734
798
|
Restart only the work explicitly approved from web status
|
|
735
799
|
|
package/dist/gateway/server.js
CHANGED
|
@@ -427,6 +427,7 @@ export class PiboGatewayServer {
|
|
|
427
427
|
listSessionRuntimeStatuses: () => this.requireRouter().listSessionRuntimeStatuses(),
|
|
428
428
|
getRuntimeCapacityStatus: () => this.requireRouter().getRuntimeCapacityStatus(),
|
|
429
429
|
listRuns: (options) => this.requireRouter().listRuns(options),
|
|
430
|
+
getRunJobReliabilityStatus: () => this.requireRouter().getRunJobReliabilityStatus(),
|
|
430
431
|
snapshotSignalSession: (piboSessionId) => this.requireRouter().snapshotSignalSession(piboSessionId),
|
|
431
432
|
snapshotSignalTree: (rootPiboSessionId) => this.requireRouter().snapshotSignalTree(rootPiboSessionId),
|
|
432
433
|
snapshotSignalStatuses: () => this.requireRouter().snapshotSignalStatuses(),
|
|
@@ -44,12 +44,14 @@ function ensurePiboJobColumn(db, name, definition) {
|
|
|
44
44
|
db.exec(`ALTER TABLE pibo_jobs ADD COLUMN ${name} ${definition}`);
|
|
45
45
|
}
|
|
46
46
|
export class PiboReliabilityStore {
|
|
47
|
+
options;
|
|
47
48
|
db;
|
|
48
49
|
appendEventStatement;
|
|
49
50
|
getEventByStreamIdStatement;
|
|
50
51
|
getEventByTopicEventIdStatement;
|
|
51
52
|
getEventByTopicIdempotencyKeyStatement;
|
|
52
|
-
constructor(path = piboHomePath("pibo-events.sqlite")) {
|
|
53
|
+
constructor(path = piboHomePath("pibo-events.sqlite"), options = {}) {
|
|
54
|
+
this.options = options;
|
|
53
55
|
const resolvedPath = path === ":memory:" ? path : resolve(path);
|
|
54
56
|
const insidePiboHome = ensurePrivatePiboHomeForPath(resolvedPath);
|
|
55
57
|
if (resolvedPath !== ":memory:")
|
|
@@ -571,6 +573,38 @@ export class PiboReliabilityStore {
|
|
|
571
573
|
}
|
|
572
574
|
return jobs;
|
|
573
575
|
}
|
|
576
|
+
inspectJobs(input = {}) {
|
|
577
|
+
const timestamp = now();
|
|
578
|
+
return this.listJobs(input).map((job) => this.inspectJob(job, timestamp));
|
|
579
|
+
}
|
|
580
|
+
reconcileOrphanRunJobs(input) {
|
|
581
|
+
const checkedAt = asDate(input.now).toISOString();
|
|
582
|
+
return this.inImmediateTransaction(() => this.reconcileOrphanRunJobsInTransaction(checkedAt, input.apply));
|
|
583
|
+
}
|
|
584
|
+
getRunJobReliabilityStatus() {
|
|
585
|
+
const timestamp = now();
|
|
586
|
+
const expired = this.db.prepare(`
|
|
587
|
+
SELECT COUNT(*) AS count
|
|
588
|
+
FROM pibo_jobs AS job
|
|
589
|
+
LEFT JOIN pibo_runs AS run ON run.job_id = job.job_id
|
|
590
|
+
WHERE job.queue = 'runs'
|
|
591
|
+
AND job.state = 'running'
|
|
592
|
+
AND job.claim_expires_at IS NOT NULL
|
|
593
|
+
AND job.claim_expires_at <= ?
|
|
594
|
+
AND run.run_id IS NULL
|
|
595
|
+
`).get(timestamp);
|
|
596
|
+
const dead = this.db.prepare(`
|
|
597
|
+
SELECT COUNT(*) AS count FROM pibo_dead_jobs
|
|
598
|
+
WHERE queue = 'runs' AND dead_reason = 'orphan_run_job'
|
|
599
|
+
`).get();
|
|
600
|
+
const expiredOrphanRunJobs = Number(expired.count);
|
|
601
|
+
const orphanRunDeadLetters = Number(dead.count);
|
|
602
|
+
return {
|
|
603
|
+
status: expiredOrphanRunJobs > 0 || orphanRunDeadLetters > 0 ? "degraded" : "ok",
|
|
604
|
+
expiredOrphanRunJobs,
|
|
605
|
+
orphanRunDeadLetters,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
574
608
|
listRecoverableJobs(input = {}) {
|
|
575
609
|
const timestamp = now();
|
|
576
610
|
const clauses = [
|
|
@@ -683,28 +717,37 @@ export class PiboReliabilityStore {
|
|
|
683
717
|
const runId = input.runId ?? `run_${randomUUID()}`;
|
|
684
718
|
const maxAttempts = Math.max(1, input.maxAttempts ?? 1);
|
|
685
719
|
const timeoutAt = input.timeoutMs === undefined ? undefined : new Date(Date.parse(timestamp) + input.timeoutMs).toISOString();
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
720
|
+
return this.inImmediateTransaction(() => {
|
|
721
|
+
const job = this.enqueue({
|
|
722
|
+
queue: "runs",
|
|
723
|
+
runAt: timestamp,
|
|
724
|
+
payload: {
|
|
725
|
+
runId,
|
|
726
|
+
controllerPiboSessionId: input.controllerPiboSessionId,
|
|
727
|
+
toolName: input.toolName,
|
|
728
|
+
params: input.params,
|
|
729
|
+
timeoutMs: input.timeoutMs,
|
|
730
|
+
},
|
|
731
|
+
maxAttempts,
|
|
732
|
+
});
|
|
733
|
+
this.options.onRunCreationBoundary?.("after_enqueue");
|
|
734
|
+
const claimed = this.claimPendingJobInTransaction(job.jobId, input.workerId ?? `run-registry:${process.pid}`, timestamp, 24 * 60 * 60 * 1000);
|
|
735
|
+
if (!claimed)
|
|
736
|
+
throw new Error(`Failed to claim newly-created run job "${job.jobId}"`);
|
|
737
|
+
this.options.onRunCreationBoundary?.("after_claim");
|
|
738
|
+
this.options.onRunCreationBoundary?.("before_run_insert");
|
|
739
|
+
this.db
|
|
740
|
+
.prepare(`
|
|
741
|
+
INSERT INTO pibo_runs (
|
|
742
|
+
run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
|
|
743
|
+
summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
|
|
744
|
+
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
|
|
745
|
+
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
746
|
+
`)
|
|
747
|
+
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
|
|
748
|
+
this.options.onRunCreationBoundary?.("after_run_insert");
|
|
749
|
+
return this.requireRun(runId);
|
|
696
750
|
});
|
|
697
|
-
this.db
|
|
698
|
-
.prepare(`
|
|
699
|
-
INSERT INTO pibo_runs (
|
|
700
|
-
run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
|
|
701
|
-
summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
|
|
702
|
-
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
|
|
703
|
-
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
704
|
-
`)
|
|
705
|
-
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
|
|
706
|
-
this.claimJob(job.jobId, input.workerId ?? `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
|
|
707
|
-
return this.requireRun(runId);
|
|
708
751
|
}
|
|
709
752
|
updateRun(runId, patch) {
|
|
710
753
|
const existing = this.getRun(runId);
|
|
@@ -779,9 +822,10 @@ export class PiboReliabilityStore {
|
|
|
779
822
|
}
|
|
780
823
|
recoverInterruptedRuns(workerId = `run-registry:${process.pid}`) {
|
|
781
824
|
return this.inImmediateTransaction(() => {
|
|
825
|
+
const timestamp = now();
|
|
826
|
+
this.reconcileOrphanRunJobsInTransaction(timestamp, true);
|
|
782
827
|
const rows = this.db.prepare("SELECT * FROM pibo_runs WHERE status = 'running'").all();
|
|
783
828
|
const recovered = [];
|
|
784
|
-
const timestamp = now();
|
|
785
829
|
for (const row of rows) {
|
|
786
830
|
if (row.job_id && this.hasUnexpiredJobClaim(row.job_id, timestamp, workerId))
|
|
787
831
|
continue;
|
|
@@ -883,6 +927,58 @@ export class PiboReliabilityStore {
|
|
|
883
927
|
`)
|
|
884
928
|
.get(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
|
|
885
929
|
}
|
|
930
|
+
claimPendingJobInTransaction(jobId, workerId, timestamp, visibilityTimeoutMs) {
|
|
931
|
+
const claimExpiresAt = new Date(Date.parse(timestamp) + visibilityTimeoutMs).toISOString();
|
|
932
|
+
return this.db.prepare(`
|
|
933
|
+
UPDATE pibo_jobs
|
|
934
|
+
SET state = 'running',
|
|
935
|
+
worker_id = ?,
|
|
936
|
+
claim_expires_at = ?,
|
|
937
|
+
claim_token = claim_token + 1,
|
|
938
|
+
attempts = attempts + 1,
|
|
939
|
+
updated_at = ?
|
|
940
|
+
WHERE job_id = ?
|
|
941
|
+
AND state = 'pending'
|
|
942
|
+
AND run_at <= ?
|
|
943
|
+
AND attempts < max_attempts
|
|
944
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
945
|
+
RETURNING *
|
|
946
|
+
`).get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
|
|
947
|
+
}
|
|
948
|
+
inspectJob(job, timestamp) {
|
|
949
|
+
const claimExpired = job.state === "running"
|
|
950
|
+
&& (!job.claimExpiresAt || job.claimExpiresAt <= timestamp);
|
|
951
|
+
const missingRunRecord = job.queue === "runs"
|
|
952
|
+
&& this.db.prepare("SELECT 1 AS found FROM pibo_runs WHERE job_id = ? LIMIT 1").get(job.jobId) === undefined;
|
|
953
|
+
let effectiveLiveness;
|
|
954
|
+
if (job.state === "pending")
|
|
955
|
+
effectiveLiveness = missingRunRecord ? "orphan_pending" : "pending";
|
|
956
|
+
else if (claimExpired)
|
|
957
|
+
effectiveLiveness = missingRunRecord ? "expired_orphan" : "expired_claim";
|
|
958
|
+
else
|
|
959
|
+
effectiveLiveness = missingRunRecord ? "live_orphan_claim" : "live_claim";
|
|
960
|
+
return { ...job, claimExpired, missingRunRecord, effectiveLiveness };
|
|
961
|
+
}
|
|
962
|
+
reconcileOrphanRunJobsInTransaction(timestamp, apply) {
|
|
963
|
+
const rows = this.db.prepare(`
|
|
964
|
+
SELECT job.*
|
|
965
|
+
FROM pibo_jobs AS job
|
|
966
|
+
LEFT JOIN pibo_runs AS run ON run.job_id = job.job_id
|
|
967
|
+
WHERE job.queue = 'runs'
|
|
968
|
+
AND job.state = 'running'
|
|
969
|
+
AND job.claim_expires_at IS NOT NULL
|
|
970
|
+
AND job.claim_expires_at <= ?
|
|
971
|
+
AND run.run_id IS NULL
|
|
972
|
+
ORDER BY job.created_at ASC, job.job_id ASC
|
|
973
|
+
`).all(timestamp);
|
|
974
|
+
const candidates = rows.map((row) => this.inspectJob(jobFromRow(row), timestamp));
|
|
975
|
+
if (apply) {
|
|
976
|
+
for (const row of rows) {
|
|
977
|
+
this.moveJobToDead(row, "Expired runs job has no matching pibo_runs record.", "orphan_run_job", timestamp);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return { checkedAt: timestamp, apply, candidates, moved: apply ? rows.length : 0 };
|
|
981
|
+
}
|
|
886
982
|
moveExpiredJobs(timestamp) {
|
|
887
983
|
const expired = this.db.prepare("SELECT * FROM pibo_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?").all(timestamp);
|
|
888
984
|
for (const row of expired)
|
|
@@ -23,8 +23,8 @@ export function buildCompactTerminalRows(traceView, options) {
|
|
|
23
23
|
const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
|
|
24
24
|
applyCompletedTurnTiming(candidates, turnById);
|
|
25
25
|
const reconciled = reconcileConceptualRowCandidates(candidates);
|
|
26
|
-
const rows =
|
|
27
|
-
? groupRelatedToolCandidates(reconciled).map((candidate) => candidate.row)
|
|
26
|
+
const rows = (options.toolDisplayMode ?? "default") === "default"
|
|
27
|
+
? groupRelatedToolCandidates(reconciled, showToolDebugMetrics).map((candidate) => candidate.row)
|
|
28
28
|
: reconciled.map((candidate) => candidate.row);
|
|
29
29
|
return applyToolDisplayMode(rows, options.toolDisplayMode ?? "default");
|
|
30
30
|
}
|
|
@@ -942,12 +942,12 @@ function isThinkingOutput(value) {
|
|
|
942
942
|
function isThinkingLevelSetOutput(value) {
|
|
943
943
|
return isRecord(value) && value.action === "set_thinking_level";
|
|
944
944
|
}
|
|
945
|
-
function groupRelatedToolCandidates(candidates) {
|
|
945
|
+
function groupRelatedToolCandidates(candidates, imagesOnly = false) {
|
|
946
946
|
const grouped = [];
|
|
947
947
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
948
948
|
const candidate = candidates[index];
|
|
949
949
|
const groupKind = candidateGroupKind(candidate);
|
|
950
|
-
if (!groupKind) {
|
|
950
|
+
if (!groupKind || (imagesOnly && groupKind !== "images")) {
|
|
951
951
|
grouped.push(candidate);
|
|
952
952
|
continue;
|
|
953
953
|
}
|
|
@@ -1028,21 +1028,20 @@ function createImageGroup(candidates) {
|
|
|
1028
1028
|
const omittedDetailCount = Math.max(0, detailItems.length - visibleDetailItems.length);
|
|
1029
1029
|
const firstRow = candidates[0]?.row;
|
|
1030
1030
|
const firstId = firstRow?.id ?? "images";
|
|
1031
|
-
const lastId = candidates[candidates.length - 1]?.row.id ?? firstId;
|
|
1032
1031
|
const status = candidates.some((candidate) => candidate.row.status === "running")
|
|
1033
1032
|
? "running"
|
|
1034
1033
|
: candidates.some((candidate) => candidate.row.status === "error")
|
|
1035
1034
|
? "error"
|
|
1036
1035
|
: "done";
|
|
1037
1036
|
return {
|
|
1038
|
-
id:
|
|
1037
|
+
id: firstId,
|
|
1039
1038
|
kind: "tool.group.images",
|
|
1040
1039
|
status,
|
|
1041
1040
|
errorKind: status === "error" ? "tool" : undefined,
|
|
1042
1041
|
lines: [
|
|
1043
1042
|
{
|
|
1044
1043
|
prefix: "bullet",
|
|
1045
|
-
tokens: [token(status === "running" ?
|
|
1044
|
+
tokens: [token(status === "running" ? `Viewing ${detailItems.length} images` : status === "error" ? `${detailItems.length} image reads · error` : `${detailItems.length} ${detailItems.length === 1 ? "Image Viewed" : "Images Viewed"}`, toneForStatus(status), "semibold")],
|
|
1046
1045
|
},
|
|
1047
1046
|
...visibleDetailItems.map((item, index) => ({
|
|
1048
1047
|
prefix: index === 0 ? "detail" : "continuation",
|
|
@@ -1062,7 +1061,7 @@ function createImageGroup(candidates) {
|
|
|
1062
1061
|
orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
|
|
1063
1062
|
detailItems,
|
|
1064
1063
|
expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
|
|
1065
|
-
imagePreviews: detailItems.flatMap((item) => item.imagePreviews ?? [])
|
|
1064
|
+
imagePreviews: detailItems.flatMap((item) => item.imagePreviews ?? []),
|
|
1066
1065
|
previewOmission: omittedDetailCount > 0 ? {
|
|
1067
1066
|
source: "details",
|
|
1068
1067
|
visibleLineCount: visibleDetailItems.length,
|
|
@@ -227,21 +227,21 @@ export class PiboDataSessionStore {
|
|
|
227
227
|
if (typeof index !== "number" || !Number.isSafeInteger(index) || index < 0)
|
|
228
228
|
continue;
|
|
229
229
|
maximum = Math.max(maximum, index);
|
|
230
|
-
persistedParts.push({ index, attributes });
|
|
230
|
+
persistedParts.push({ index, type: row.type, attributes });
|
|
231
231
|
}
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
232
|
+
// A completed turn or an exact terminal-part fingerprint is durable
|
|
233
|
+
// evidence for replay. Nonterminal parts remain unattached while the turn
|
|
234
|
+
// is open because another producer may still own them.
|
|
235
|
+
const matchesReplay = ({ type, attributes }) => (turnCompleted || outputPartEventIsTerminal(type))
|
|
236
|
+
&& (attributes.outputPartFingerprint === input.fingerprint
|
|
237
|
+
|| attributes.identityFingerprint === input.identityFingerprint);
|
|
238
|
+
const replay = (input.suppliedIndex === undefined
|
|
239
|
+
? undefined
|
|
240
|
+
: persistedParts.find((part) => part.index === input.suppliedIndex && matchesReplay(part)))
|
|
241
|
+
?? (turnCompleted ? persistedParts.find(matchesReplay) : undefined);
|
|
242
|
+
if (replay) {
|
|
243
|
+
this.observeOutputPartIndex(input, replay.index);
|
|
244
|
+
return replay.index;
|
|
245
245
|
}
|
|
246
246
|
const minimum = Math.max(input.proposedIndex, maximum + 1);
|
|
247
247
|
const row = this.db.prepare(`
|
|
@@ -538,6 +538,7 @@ export class PiboDataSessionStore {
|
|
|
538
538
|
actorId: session.id,
|
|
539
539
|
event,
|
|
540
540
|
createdAt: at,
|
|
541
|
+
persistenceProvenance: { producer: "runtime-recovery", projection: "product-history", phase: "startup-recovery" },
|
|
541
542
|
});
|
|
542
543
|
results.push({
|
|
543
544
|
turnId: recovered.turn.turnId,
|
|
@@ -726,6 +727,9 @@ function outputPartEventTypes(kind) {
|
|
|
726
727
|
case "compaction": return ["compaction_start", "compaction_end"];
|
|
727
728
|
}
|
|
728
729
|
}
|
|
730
|
+
function outputPartEventIsTerminal(type) {
|
|
731
|
+
return type === "assistant_message" || type === "thinking_finished" || type === "assistant_usage" || type === "compaction_end";
|
|
732
|
+
}
|
|
729
733
|
function parseJsonObject(json) {
|
|
730
734
|
if (!json)
|
|
731
735
|
return {};
|