@fungi.computer/shiitake 0.2.0 → 0.3.1
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/README.md +155 -34
- package/dist/agent-control.d.ts +77 -5
- package/dist/agent-control.js +271 -65
- package/dist/client-entry.d.ts +15 -8
- package/dist/client-entry.js +7 -5
- package/dist/client-wire.d.ts +24 -8
- package/dist/client-wire.js +53 -11
- package/dist/client.d.ts +56 -11
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +42 -23
- package/dist/cloudflare.js +76 -54
- package/dist/cold-sessions.d.ts +28 -0
- package/dist/cold-sessions.js +1 -0
- package/dist/delegation.d.ts +214 -0
- package/dist/delegation.js +319 -0
- package/dist/effect.d.ts +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/extension-contract.d.ts +10 -2
- package/dist/extensions.d.ts +6 -2
- package/dist/fleet.d.ts +43 -9
- package/dist/fleet.js +88 -25
- package/dist/hosted.js +1 -0
- package/dist/http-routes.d.ts +113 -29
- package/dist/http-routes.js +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/internal/accepted-run-executor.d.ts +5 -0
- package/dist/internal/accepted-run-executor.js +10 -4
- package/dist/internal/admission-door.d.ts +3 -0
- package/dist/internal/admission-door.js +12 -2
- package/dist/internal/client-boundary.js +7 -1
- package/dist/internal/compaction/compaction-slot.d.ts +36 -0
- package/dist/internal/compaction/compaction-slot.js +41 -0
- package/dist/internal/compaction/compaction.d.ts +11 -8
- package/dist/internal/compaction/compaction.js +62 -34
- package/dist/internal/compaction/session-compaction.d.ts +8 -16
- package/dist/internal/compaction/session-compaction.js +358 -119
- package/dist/internal/compaction/settings.d.ts +1 -1
- package/dist/internal/compaction/settings.js +5 -0
- package/dist/internal/compaction/utils.d.ts +19 -16
- package/dist/internal/compaction/utils.js +30 -84
- package/dist/internal/contracts.d.ts +40 -13
- package/dist/internal/coordinator.js +28 -10
- package/dist/internal/effect-client.d.ts +4 -2
- package/dist/internal/effect-client.js +29 -0
- package/dist/internal/effect-contracts.d.ts +17 -4
- package/dist/internal/extension-runtime.d.ts +5 -0
- package/dist/internal/extension-runtime.js +184 -89
- package/dist/internal/host-meta.d.ts +7 -0
- package/dist/internal/model-acquisition.js +7 -1
- package/dist/internal/model-preference.d.ts +4 -2
- package/dist/internal/model-preference.js +2 -1
- package/dist/internal/promise-facade.js +4 -16
- package/dist/internal/run-failure-reason.d.ts +10 -0
- package/dist/internal/run-failure-reason.js +19 -0
- package/dist/internal/run-scope.d.ts +4 -1
- package/dist/internal/run-scope.js +38 -5
- package/dist/internal/runtime-coordinator.d.ts +2 -1
- package/dist/internal/runtime-coordinator.js +65 -4
- package/dist/internal/runtime-core.js +1 -0
- package/dist/internal/runtime-reconciliation.js +42 -6
- package/dist/internal/session-context.d.ts +1 -2
- package/dist/internal/session-context.js +8 -30
- package/dist/internal/session-status.d.ts +2 -1
- package/dist/internal/session-status.js +4 -1
- package/dist/internal/woodstock/client-projection.d.ts +6 -5
- package/dist/internal/woodstock/client-projection.js +79 -53
- package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
- package/dist/internal/woodstock/cold-sessions.js +90 -0
- package/dist/internal/woodstock/extension-store.d.ts +2 -0
- package/dist/internal/woodstock/extension-store.js +4 -0
- package/dist/internal/woodstock/fleet-store.d.ts +5 -1
- package/dist/internal/woodstock/fleet-store.js +17 -5
- package/dist/internal/woodstock/index.d.ts +11 -3
- package/dist/internal/woodstock/index.js +33 -14
- package/dist/internal/woodstock/progress.d.ts +14 -1
- package/dist/internal/woodstock/progress.js +97 -19
- package/dist/internal/woodstock/run-recovery.js +6 -4
- package/dist/internal/woodstock/session-attention.d.ts +1 -1
- package/dist/internal/woodstock/session-attention.js +2 -2
- package/dist/internal/woodstock/session-entry.d.ts +110 -43
- package/dist/internal/woodstock/session-entry.js +63 -20
- package/dist/internal/woodstock/session-inbox.js +2 -0
- package/dist/internal/woodstock/session-store.d.ts +104 -24
- package/dist/internal/woodstock/session-store.js +937 -246
- package/dist/internal/woodstock/storage-admission.d.ts +8 -8
- package/dist/internal/woodstock/storage-admission.js +8 -34
- package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
- package/dist/internal/woodstock/storage-conformance.js +489 -0
- package/dist/model-preference.d.ts +2 -2
- package/dist/model-preference.js +2 -2
- package/dist/node.js +1 -1
- package/dist/remote-client.js +8 -1
- package/dist/session-attention.d.ts +8 -1
- package/dist/session-attention.js +6 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -0
- package/docs/api/README.md +18 -16
- package/docs/api/agent-control.md +171 -1
- package/docs/api/client-entry.md +26 -10
- package/docs/api/client.md +153 -37
- package/docs/api/cloudflare.md +76 -23
- package/docs/api/delegation.md +485 -0
- package/docs/api/effect.md +20 -2
- package/docs/api/extensions.md +21 -9
- package/docs/api/fleet.md +159 -104
- package/docs/api/http.md +4 -1
- package/docs/api/index.md +116 -20
- package/docs/api/internal/woodstock/storage-conformance.md +81 -0
- package/docs/api/model-preference.md +27 -10
- package/docs/api/testing.md +16 -4
- package/package.json +26 -14
- package/dist/internal/browser-sessions.d.ts +0 -31
- package/dist/internal/browser-sessions.js +0 -173
|
@@ -56,6 +56,7 @@ const EntryRowSchema = Schema.Struct({
|
|
|
56
56
|
data_json: Schema.String,
|
|
57
57
|
timestamp,
|
|
58
58
|
completed_at: Schema.NullOr(timestamp),
|
|
59
|
+
damaged: Schema.Literals([0, 1]),
|
|
59
60
|
});
|
|
60
61
|
const CommandAdmissionRowSchema = Schema.Struct({
|
|
61
62
|
id: boundedId,
|
|
@@ -100,7 +101,6 @@ const admissionColumns = (alias) => [
|
|
|
100
101
|
.join(", ");
|
|
101
102
|
const NextSequenceRowSchema = Schema.Struct({ next_sequence: sequence });
|
|
102
103
|
const EntryPresenceRowSchema = Schema.Struct({ present: Schema.Literal(1) });
|
|
103
|
-
const decodeSessionRow = Schema.decodeUnknownResult(SessionRowSchema, closed);
|
|
104
104
|
const decodeSessionCreationReceiptRow = Schema.decodeUnknownResult(SessionCreationReceiptRowSchema, closed);
|
|
105
105
|
const decodeSessionSummaryRow = Schema.decodeUnknownResult(SessionSummaryRowSchema, closed);
|
|
106
106
|
const decodeSessionIdRow = Schema.decodeUnknownResult(SessionIdRowSchema, closed);
|
|
@@ -116,9 +116,29 @@ const SummaryLineageSchema = Schema.Struct({
|
|
|
116
116
|
const decodeSummaryLineage = Schema.decodeUnknownResult(SummaryLineageSchema, closed);
|
|
117
117
|
const decodeEntryRow = Schema.decodeUnknownResult(EntryRowSchema, closed);
|
|
118
118
|
const decodeCommandAdmissionRow = Schema.decodeUnknownResult(CommandAdmissionRowSchema, closed);
|
|
119
|
-
const decodeModelChoiceRow = Schema.decodeUnknownResult(Schema.Struct({ sequence: Schema.NullOr(sequence) }), closed);
|
|
120
119
|
const decodeNextSequenceRow = Schema.decodeUnknownResult(NextSequenceRowSchema, closed);
|
|
121
120
|
const decodeEntryPresenceRow = Schema.decodeUnknownResult(EntryPresenceRowSchema, closed);
|
|
121
|
+
/** The columns of an entry row from `table`, with whether its Session is damaged. */
|
|
122
|
+
function entryColumnsOf(table) {
|
|
123
|
+
const columns = [
|
|
124
|
+
"id",
|
|
125
|
+
"session_id",
|
|
126
|
+
"parent_id",
|
|
127
|
+
"sequence",
|
|
128
|
+
"type",
|
|
129
|
+
"role",
|
|
130
|
+
"state",
|
|
131
|
+
"request_id",
|
|
132
|
+
"work_id",
|
|
133
|
+
"data_json",
|
|
134
|
+
"timestamp",
|
|
135
|
+
"completed_at",
|
|
136
|
+
].map((column) => `${table}.${column}`);
|
|
137
|
+
return `${columns.join(", ")},
|
|
138
|
+
EXISTS (SELECT 1 FROM session_lineage_damage AS damage
|
|
139
|
+
WHERE damage.session_id = ${table}.session_id) AS damaged`;
|
|
140
|
+
}
|
|
141
|
+
const entryColumns = entryColumnsOf("session_entries");
|
|
122
142
|
/**
|
|
123
143
|
* Page stored conversation facts without creating Sessions or opening hosts.
|
|
124
144
|
*
|
|
@@ -167,6 +187,8 @@ export function readStoredSessionFactPage(owner, after) {
|
|
|
167
187
|
* title derived from the first entered prompt.
|
|
168
188
|
*/
|
|
169
189
|
export function readStoredSessionSummary(owner, sessionId) {
|
|
190
|
+
if (readSessionRow(owner, sessionId) === undefined)
|
|
191
|
+
return undefined;
|
|
170
192
|
const raw = owner.sql
|
|
171
193
|
.exec(`SELECT id, leaf_id, created_at, updated_at, metadata_json
|
|
172
194
|
FROM sessions
|
|
@@ -328,12 +350,6 @@ function unavailable(message) {
|
|
|
328
350
|
function incompatible(message) {
|
|
329
351
|
return new ShiitakeError("store", "incompatible", message);
|
|
330
352
|
}
|
|
331
|
-
function parseSessionRow(raw) {
|
|
332
|
-
const decoded = decodeSessionRow(raw);
|
|
333
|
-
if (decoded._tag === "Failure")
|
|
334
|
-
throw unavailable("Stored Session row is invalid");
|
|
335
|
-
return decoded.success;
|
|
336
|
-
}
|
|
337
353
|
function requestIdFor(entry) {
|
|
338
354
|
return entry.kind === "command" ||
|
|
339
355
|
entry.kind === "cancellation" ||
|
|
@@ -359,11 +375,18 @@ function sameCausalIdentity(left, right) {
|
|
|
359
375
|
left.parent?.kind === right.parent?.kind &&
|
|
360
376
|
left.parent?.value === right.parent?.value);
|
|
361
377
|
}
|
|
362
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Decode one entry row. Every entry read selects its Session's damage with
|
|
380
|
+
* it (`entryColumns`), so a row of a Session the lineage audit refused is
|
|
381
|
+
* refused here, whichever reader asked for it.
|
|
382
|
+
*/
|
|
383
|
+
function parseEntryRow(owner, raw) {
|
|
363
384
|
const decodedRow = decodeEntryRow(raw);
|
|
364
385
|
if (decodedRow._tag === "Failure")
|
|
365
386
|
throw unavailable("Stored Session entry row is invalid");
|
|
366
387
|
const row = decodedRow.success;
|
|
388
|
+
if (row.damaged === 1)
|
|
389
|
+
throw brokenSession(owner, row.session_id);
|
|
367
390
|
const decodedEntry = decodeStoredSessionEntry(row.data_json);
|
|
368
391
|
if (decodedEntry._tag === "Failure")
|
|
369
392
|
throw unavailable("Stored Session entry data is invalid");
|
|
@@ -424,20 +447,41 @@ function parseCommandAdmissionRow(raw) {
|
|
|
424
447
|
export function hasStoredSession(owner, sessionId) {
|
|
425
448
|
return readSessionRow(owner, sessionId) !== undefined;
|
|
426
449
|
}
|
|
450
|
+
const decodeReadableSessionRow = Schema.decodeUnknownResult(Schema.Struct({
|
|
451
|
+
...SessionRowSchema.fields,
|
|
452
|
+
damaged: Schema.Literals([0, 1]),
|
|
453
|
+
}), closed);
|
|
454
|
+
/**
|
|
455
|
+
* The Session row every reader and writer of one Session starts from. A
|
|
456
|
+
* Session the lineage audit found broken is refused here, so no read of it
|
|
457
|
+
* and no write to it gets further.
|
|
458
|
+
*/
|
|
427
459
|
function readSessionRow(owner, sessionId) {
|
|
428
460
|
const raw = owner.sql
|
|
429
|
-
.exec(
|
|
461
|
+
.exec(`SELECT id, leaf_id,
|
|
462
|
+
EXISTS (SELECT 1 FROM session_lineage_damage
|
|
463
|
+
WHERE session_id = sessions.id) AS damaged
|
|
464
|
+
FROM sessions WHERE id = ?`, sessionId)
|
|
430
465
|
.toArray()[0];
|
|
431
|
-
|
|
466
|
+
if (raw === undefined)
|
|
467
|
+
return undefined;
|
|
468
|
+
const decoded = decodeReadableSessionRow(raw);
|
|
469
|
+
if (decoded._tag === "Failure")
|
|
470
|
+
throw unavailable("Stored Session row is invalid");
|
|
471
|
+
if (decoded.success.damaged === 1)
|
|
472
|
+
throw brokenSession(owner, sessionId);
|
|
473
|
+
return Object.freeze({
|
|
474
|
+
id: decoded.success.id,
|
|
475
|
+
leaf_id: decoded.success.leaf_id,
|
|
476
|
+
});
|
|
432
477
|
}
|
|
433
478
|
function readEntryRow(owner, sessionId, id) {
|
|
434
479
|
const raw = owner.sql
|
|
435
|
-
.exec(`SELECT
|
|
436
|
-
request_id, work_id, data_json, timestamp, completed_at
|
|
480
|
+
.exec(`SELECT ${entryColumns}
|
|
437
481
|
FROM session_entries
|
|
438
482
|
WHERE session_id = ? AND id = ?`, sessionId, id)
|
|
439
483
|
.toArray()[0];
|
|
440
|
-
return raw === undefined ? undefined : parseEntryRow(raw);
|
|
484
|
+
return raw === undefined ? undefined : parseEntryRow(owner, raw);
|
|
441
485
|
}
|
|
442
486
|
function validateCompleteSessionLeaf(owner, session) {
|
|
443
487
|
if (session.leaf_id === null) {
|
|
@@ -462,12 +506,11 @@ function validateCompleteSessionLeaf(owner, session) {
|
|
|
462
506
|
/** Read one retained Session entry by its globally unique row identity. */
|
|
463
507
|
export function readSessionEntryById(owner, id) {
|
|
464
508
|
const raw = owner.sql
|
|
465
|
-
.exec(`SELECT
|
|
466
|
-
request_id, work_id, data_json, timestamp, completed_at
|
|
509
|
+
.exec(`SELECT ${entryColumns}
|
|
467
510
|
FROM session_entries
|
|
468
511
|
WHERE id = ?`, id)
|
|
469
512
|
.toArray()[0];
|
|
470
|
-
return raw === undefined ? undefined : parseEntryRow(raw);
|
|
513
|
+
return raw === undefined ? undefined : parseEntryRow(owner, raw);
|
|
471
514
|
}
|
|
472
515
|
function readCommandAdmission(owner, sessionId, lookup) {
|
|
473
516
|
const [column, value] = "id" in lookup
|
|
@@ -491,6 +534,36 @@ const pendingAdmissions = `FROM session_command_admissions AS a
|
|
|
491
534
|
WHERE e.session_id = a.session_id AND e.work_id = a.work_id
|
|
492
535
|
AND e.type IN ('command', 'cancellation', 'run_terminal', 'work_terminal')
|
|
493
536
|
)`;
|
|
537
|
+
const decodeIdentity = Schema.decodeUnknownSync(Schema.Struct({ identity: Schema.NullOr(Schema.String) }));
|
|
538
|
+
/** Extract the host-selected principal without giving the engine domain grammar. */
|
|
539
|
+
export function readAdmissionIdentity(owner, path, meta) {
|
|
540
|
+
const { identity } = decodeIdentity(owner.sql
|
|
541
|
+
.exec("SELECT json_extract(?, ?) AS identity", JSON.stringify(meta ?? null), path)
|
|
542
|
+
.one());
|
|
543
|
+
if (identity === null || identity.length === 0)
|
|
544
|
+
throw new ShiitakeError("runtime", "rejected", "Request author is required");
|
|
545
|
+
return { path, value: identity };
|
|
546
|
+
}
|
|
547
|
+
/** Immutable admission author check; used before every exact cancellation, including replay. */
|
|
548
|
+
export function assertAdmissionIdentity(owner, sessionId, commandId, identity) {
|
|
549
|
+
const admission = readSessionCommandAdmission(owner, sessionId, commandId);
|
|
550
|
+
if (admission === undefined)
|
|
551
|
+
return;
|
|
552
|
+
const stored = readAdmissionIdentity(owner, identity.path, admission.entry.meta);
|
|
553
|
+
if (stored.value !== identity.value)
|
|
554
|
+
throw new ShiitakeError("runtime", "rejected", "Message author does not match");
|
|
555
|
+
}
|
|
556
|
+
/** Read just one newest admission; FIFO execution and oldest-first paging are unchanged. */
|
|
557
|
+
export function readNewestPendingSessionCommand(owner, sessionId, identity) {
|
|
558
|
+
const raw = owner.sql
|
|
559
|
+
.exec(`SELECT ${admissionColumns("a.")} ${pendingAdmissions}
|
|
560
|
+
AND json_extract(a.data_json, '$.work') IS NULL
|
|
561
|
+
AND a.origin_kind = 'user'
|
|
562
|
+
AND (? IS NULL OR json_extract(json_extract(a.data_json, '$.meta'), ?) = ?)
|
|
563
|
+
ORDER BY a.sequence DESC LIMIT 1`, sessionId, identity?.path ?? null, identity?.path ?? null, identity?.value ?? null)
|
|
564
|
+
.toArray()[0];
|
|
565
|
+
return raw === undefined ? undefined : parseCommandAdmissionRow(raw);
|
|
566
|
+
}
|
|
494
567
|
const decodePendingCount = Schema.decodeUnknownSync(Schema.Struct({
|
|
495
568
|
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
496
569
|
}));
|
|
@@ -575,14 +648,22 @@ export function settleSessionControls(owner, input) {
|
|
|
575
648
|
});
|
|
576
649
|
continue;
|
|
577
650
|
}
|
|
651
|
+
// Only a delivered control shares its failed parent's safe reason.
|
|
652
|
+
// Late, unmaterialized work was requeued above and owns its own outcome.
|
|
653
|
+
const diagnostic = input.outcome === "failed" || input.outcome === "interrupted"
|
|
654
|
+
? input.diagnostic
|
|
655
|
+
: undefined;
|
|
656
|
+
let entry = {
|
|
657
|
+
kind: "work_terminal",
|
|
658
|
+
workId: row.entry.workId,
|
|
659
|
+
outcome: input.outcome,
|
|
660
|
+
};
|
|
661
|
+
if (diagnostic !== undefined)
|
|
662
|
+
entry = { ...entry, diagnostic };
|
|
578
663
|
appendSessionEntry(owner, {
|
|
579
664
|
sessionId: input.sessionId,
|
|
580
665
|
id: sessionIds.fact.generate(),
|
|
581
|
-
entry
|
|
582
|
-
kind: "work_terminal",
|
|
583
|
-
workId: row.entry.workId,
|
|
584
|
-
outcome: input.outcome,
|
|
585
|
-
},
|
|
666
|
+
entry,
|
|
586
667
|
});
|
|
587
668
|
}
|
|
588
669
|
});
|
|
@@ -601,20 +682,21 @@ export function readSessionCommandAdmissionByRequest(owner, sessionId, requestId
|
|
|
601
682
|
}
|
|
602
683
|
/** Read entries correlated to one accepted work identity in Session order. */
|
|
603
684
|
export function readSessionEntriesForWork(owner, sessionId, workId) {
|
|
685
|
+
// Refuse the Session before selecting entries: a damaged work may have no
|
|
686
|
+
// rows left, so decoding the result cannot establish that it is readable.
|
|
687
|
+
readSessionRow(owner, sessionId);
|
|
604
688
|
return Object.freeze(owner.sql
|
|
605
|
-
.exec(`SELECT
|
|
606
|
-
request_id, work_id, data_json, timestamp, completed_at
|
|
689
|
+
.exec(`SELECT ${entryColumns}
|
|
607
690
|
FROM session_entries
|
|
608
691
|
WHERE session_id = ? AND work_id = ?
|
|
609
692
|
ORDER BY sequence ASC`, sessionId, workId)
|
|
610
693
|
.toArray()
|
|
611
|
-
.map(parseEntryRow));
|
|
694
|
+
.map((raw) => parseEntryRow(owner, raw)));
|
|
612
695
|
}
|
|
613
696
|
/** Read the latest retained live-retry event for one accepted work item. */
|
|
614
697
|
export function readLatestLiveRetryForWork(owner, sessionId, workId) {
|
|
615
698
|
const raw = owner.sql
|
|
616
|
-
.exec(`SELECT
|
|
617
|
-
request_id, work_id, data_json, timestamp, completed_at
|
|
699
|
+
.exec(`SELECT ${entryColumns}
|
|
618
700
|
FROM session_entries
|
|
619
701
|
WHERE session_id = ? AND work_id = ? AND type = 'live_retry'
|
|
620
702
|
ORDER BY sequence DESC
|
|
@@ -622,7 +704,7 @@ export function readLatestLiveRetryForWork(owner, sessionId, workId) {
|
|
|
622
704
|
.toArray()[0];
|
|
623
705
|
if (raw === undefined)
|
|
624
706
|
return undefined;
|
|
625
|
-
const row = parseEntryRow(raw);
|
|
707
|
+
const row = parseEntryRow(owner, raw);
|
|
626
708
|
if (row.entry.kind !== "live_retry")
|
|
627
709
|
throw unavailable("Stored live retry entry is invalid");
|
|
628
710
|
return { event: row.entry.event, timestamp: row.timestamp };
|
|
@@ -639,10 +721,7 @@ export function hasSuccessfulToolResult(owner, sessionId, workId, toolCallId) {
|
|
|
639
721
|
/** Most recent tool intent without a matching outcome for the running work. */
|
|
640
722
|
export function readActiveToolForWork(owner, sessionId, workId) {
|
|
641
723
|
const raw = owner.sql
|
|
642
|
-
.exec(`SELECT intent
|
|
643
|
-
intent.sequence, intent.type, intent.role, intent.state,
|
|
644
|
-
intent.request_id, intent.work_id, intent.data_json,
|
|
645
|
-
intent.timestamp, intent.completed_at
|
|
724
|
+
.exec(`SELECT ${entryColumnsOf("intent")}
|
|
646
725
|
FROM session_entries AS intent
|
|
647
726
|
WHERE intent.session_id = ? AND intent.work_id = ?
|
|
648
727
|
AND intent.type = 'tool_intent'
|
|
@@ -659,7 +738,7 @@ export function readActiveToolForWork(owner, sessionId, workId) {
|
|
|
659
738
|
.toArray()[0];
|
|
660
739
|
if (raw === undefined)
|
|
661
740
|
return undefined;
|
|
662
|
-
const row = parseEntryRow(raw);
|
|
741
|
+
const row = parseEntryRow(owner, raw);
|
|
663
742
|
if (row.entry.kind !== "tool_intent")
|
|
664
743
|
throw unavailable("Stored tool intent is invalid");
|
|
665
744
|
return row.entry.call.name;
|
|
@@ -737,12 +816,15 @@ admitNew) {
|
|
|
737
816
|
*/
|
|
738
817
|
export function deleteStoredSession(owner, input) {
|
|
739
818
|
return owner.transactionSync(() => {
|
|
740
|
-
|
|
819
|
+
// A Session the lineage audit refused can still be deleted.
|
|
820
|
+
const exists = owner.sql
|
|
821
|
+
.exec("SELECT id FROM sessions WHERE id = ?", input.sessionId)
|
|
822
|
+
.toArray().length > 0;
|
|
823
|
+
if (!exists)
|
|
741
824
|
return false;
|
|
742
825
|
input.assertIdleSession(input.sessionId);
|
|
743
826
|
input.recordInboxDeletion(input.sessionId);
|
|
744
827
|
for (const statement of [
|
|
745
|
-
"DELETE FROM session_entries WHERE session_id = ?",
|
|
746
828
|
"DELETE FROM session_command_admissions WHERE session_id = ?",
|
|
747
829
|
// The inbox row references the Session; it goes in the same transaction.
|
|
748
830
|
"DELETE FROM session_attention WHERE session_id = ?",
|
|
@@ -750,13 +832,20 @@ export function deleteStoredSession(owner, input) {
|
|
|
750
832
|
"DELETE FROM session_inbox WHERE session_id = ?",
|
|
751
833
|
"DELETE FROM session_creation_receipts WHERE session_id = ?",
|
|
752
834
|
"DELETE FROM sessions WHERE id = ?",
|
|
835
|
+
// Entries go last: only a removed Session's lineage may lose parents.
|
|
836
|
+
"DELETE FROM session_entries WHERE session_id = ?",
|
|
837
|
+
"DELETE FROM session_lineage_damage WHERE session_id = ?",
|
|
753
838
|
])
|
|
754
839
|
owner.sql.exec(statement, input.sessionId);
|
|
755
840
|
return true;
|
|
756
841
|
});
|
|
757
842
|
}
|
|
758
|
-
/**
|
|
759
|
-
|
|
843
|
+
/**
|
|
844
|
+
* Version 3 keeps application tool data outside model messages/results.
|
|
845
|
+
* Older preview stores require explicit host custody; no compatibility reader
|
|
846
|
+
* or automatic rewrite runs when the store opens.
|
|
847
|
+
*/
|
|
848
|
+
const currentWoodstockSchemaVersion = 3;
|
|
760
849
|
const currentWoodstockProtocol = "shiitake.extension/v0";
|
|
761
850
|
function tableExists(owner, name) {
|
|
762
851
|
const rows = owner.sql
|
|
@@ -786,10 +875,12 @@ function readWoodstockStoreProtocol(owner) {
|
|
|
786
875
|
if (rows.length !== 1)
|
|
787
876
|
return "unmarked";
|
|
788
877
|
const decoded = decodeWoodstockSchemaVersionRow(rows[0]);
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
878
|
+
if (decoded._tag !== "Success")
|
|
879
|
+
return "unmarked";
|
|
880
|
+
// The decoder already requires the current protocol literal.
|
|
881
|
+
if (decoded.success.version === currentWoodstockSchemaVersion)
|
|
882
|
+
return "v0";
|
|
883
|
+
return "unmarked";
|
|
793
884
|
}
|
|
794
885
|
/** Stamp fresh Woodstock stores; incompatible files require host-side custody. */
|
|
795
886
|
function ensureWoodstockSchemaVersion(owner) {
|
|
@@ -797,7 +888,7 @@ function ensureWoodstockSchemaVersion(owner) {
|
|
|
797
888
|
if (protocol === "v0")
|
|
798
889
|
return;
|
|
799
890
|
if (protocol === "unmarked")
|
|
800
|
-
throw incompatible("Woodstock store has no v0
|
|
891
|
+
throw incompatible("Woodstock store has no current v0 schema marker; incompatible storage requires explicit host custody. To reset, stop the owning host before resetting a local store, then remove its database file and the file's .owner.sqlite sidecar. Reset discards that store's data. For the Hub, a Hub Agent is erased only with its Team; Team erasure is destructive and separately authorized, never a routine single-Agent repair. No migration runs.");
|
|
801
892
|
owner.sql.exec("CREATE TABLE woodstock_schema (version INTEGER NOT NULL, protocol TEXT NOT NULL)");
|
|
802
893
|
owner.sql.exec("INSERT INTO woodstock_schema (version, protocol) VALUES (?, ?)", currentWoodstockSchemaVersion, currentWoodstockProtocol);
|
|
803
894
|
}
|
|
@@ -820,6 +911,201 @@ function createSessionEntryTable(owner) {
|
|
|
820
911
|
)
|
|
821
912
|
`);
|
|
822
913
|
}
|
|
914
|
+
const lineageGuards = [
|
|
915
|
+
"session_entries_lineage_insert",
|
|
916
|
+
"session_entries_lineage_update",
|
|
917
|
+
"session_entries_lineage_delete",
|
|
918
|
+
"session_entries_damaged_session",
|
|
919
|
+
"session_entries_lineage_replace",
|
|
920
|
+
"session_entries_lineage_replace_position",
|
|
921
|
+
];
|
|
922
|
+
const decodeInstalledGuards = Schema.decodeUnknownResult(Schema.Struct({
|
|
923
|
+
installed: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
924
|
+
}), closed);
|
|
925
|
+
const decodeDamagedSession = Schema.decodeUnknownResult(Schema.Struct({ session_id: Schema.String }), closed);
|
|
926
|
+
/** What a writer to a Session the lineage audit found broken is told. */
|
|
927
|
+
const brokenSessionMessage = "Woodstock Session holds a broken lineage; a store never repairs itself — delete this Session (deleteSession), or reset the owning store";
|
|
928
|
+
const decodeDamageCount = Schema.decodeUnknownResult(Schema.Struct({
|
|
929
|
+
damaged: Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0))),
|
|
930
|
+
first: Schema.String,
|
|
931
|
+
}), closed);
|
|
932
|
+
/**
|
|
933
|
+
* The refusal of every read of, and write to, a Session the lineage audit
|
|
934
|
+
* found broken. Work settled for it carries `diagnostic`, a bounded form of
|
|
935
|
+
* the message that still names the Session and its reset.
|
|
936
|
+
*/
|
|
937
|
+
export class BrokenSessionError extends ShiitakeError {
|
|
938
|
+
store;
|
|
939
|
+
sessionId;
|
|
940
|
+
constructor(store, sessionId, message) {
|
|
941
|
+
super("store", "unavailable", message);
|
|
942
|
+
this.store = store;
|
|
943
|
+
this.sessionId = sessionId;
|
|
944
|
+
}
|
|
945
|
+
get diagnostic() {
|
|
946
|
+
return `${this.store}: Woodstock Session ${this.sessionId} holds a broken lineage; delete it with deleteSession("${this.sessionId}"), or reset the owning store`;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/** The refusal of one Session the lineage audit found broken, naming its store. */
|
|
950
|
+
function brokenSession(owner, sessionId) {
|
|
951
|
+
const store = owner.storeName ?? "Woodstock store";
|
|
952
|
+
return new BrokenSessionError(store, sessionId, `${store}: ${brokenSessionRefusal(owner, sessionId)}`);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Whether the lineage audit refused this Session. Settlement asks before it
|
|
956
|
+
* records anything in the Session, which it can no longer write.
|
|
957
|
+
*/
|
|
958
|
+
export function sessionLineageRefused(owner, sessionId) {
|
|
959
|
+
return (owner.sql
|
|
960
|
+
.exec("SELECT session_id FROM session_lineage_damage WHERE session_id = ?", sessionId)
|
|
961
|
+
.toArray().length > 0);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* What a reader of a broken Session is told: which Session, how many of this
|
|
965
|
+
* store's Sessions the audit found broken and the first of them, and the
|
|
966
|
+
* steps that clear it.
|
|
967
|
+
*/
|
|
968
|
+
function brokenSessionRefusal(owner, sessionId) {
|
|
969
|
+
const decoded = decodeDamageCount(owner.sql
|
|
970
|
+
.exec("SELECT COUNT(*) AS damaged, MIN(session_id) AS first FROM session_lineage_damage")
|
|
971
|
+
.one());
|
|
972
|
+
if (decoded._tag === "Failure")
|
|
973
|
+
throw unavailable("Stored Session lineage damage is invalid");
|
|
974
|
+
const { damaged, first } = decoded.success;
|
|
975
|
+
return `Woodstock Session ${sessionId} holds a broken lineage (this store has ${String(damaged)} damaged Session${damaged === 1 ? "" : "s"}, first ${first}); a store never repairs itself — delete this Session with deleteSession("${sessionId}"), or reset the owning store: for a Durable Object, erase all of its storage (DurableObjectStorage.deleteAll(); a Hub Agent is erased only with its Team, POST /operator/teams/:teamId/erasure-durable-objects); for a local store, remove its file and the file's .owner.sqlite sidecar`;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Every lineage stays whole by construction, enforced where it is written
|
|
979
|
+
* rather than re-walked where it is read. Each entry's parent is an earlier
|
|
980
|
+
* complete entry of its own Session, each Session has one complete root, the
|
|
981
|
+
* lineage columns never change, and a parent is never removed from under a
|
|
982
|
+
* child while its Session exists. So every walk from a complete leaf reaches
|
|
983
|
+
* that one root over complete rows of falling sequence, and readers walk only
|
|
984
|
+
* the rows they return.
|
|
985
|
+
*
|
|
986
|
+
* A store written while any guard was missing is audited as they are
|
|
987
|
+
* installed. Each Session that breaks them is recorded in
|
|
988
|
+
* `session_lineage_damage`, in the same pass: its inbox row turns
|
|
989
|
+
* unavailable, every read of it is refused and no entry is written to it,
|
|
990
|
+
* while every other Session opens and works as before. The guards only judge
|
|
991
|
+
* later writes, and each judges one Session, so a damaged Session never
|
|
992
|
+
* blocks installing them or writing elsewhere. Deleting the Session clears it.
|
|
993
|
+
*/
|
|
994
|
+
function guardSessionLineage(owner) {
|
|
995
|
+
owner.sql.exec("CREATE TABLE IF NOT EXISTS session_lineage_damage (session_id TEXT PRIMARY KEY)");
|
|
996
|
+
const installed = decodeInstalledGuards(owner.sql
|
|
997
|
+
.exec(`SELECT COUNT(*) AS installed FROM sqlite_master
|
|
998
|
+
WHERE type = 'trigger'
|
|
999
|
+
AND name IN (SELECT value FROM json_each(?))`, JSON.stringify(lineageGuards))
|
|
1000
|
+
.one());
|
|
1001
|
+
if (installed._tag === "Failure")
|
|
1002
|
+
throw unavailable("Stored Session lineage guards are invalid");
|
|
1003
|
+
if (installed.success.installed !== lineageGuards.length) {
|
|
1004
|
+
const damaged = owner.sql
|
|
1005
|
+
.exec(`SELECT child.session_id AS session_id
|
|
1006
|
+
FROM session_entries AS child
|
|
1007
|
+
LEFT JOIN session_entries AS parent ON parent.id = child.parent_id
|
|
1008
|
+
WHERE child.parent_id IS NOT NULL
|
|
1009
|
+
AND (parent.id IS NULL
|
|
1010
|
+
OR parent.session_id != child.session_id
|
|
1011
|
+
OR parent.state != 'complete'
|
|
1012
|
+
OR parent.sequence >= child.sequence)
|
|
1013
|
+
UNION
|
|
1014
|
+
SELECT session_id FROM session_entries
|
|
1015
|
+
WHERE parent_id IS NULL AND state = 'complete'
|
|
1016
|
+
GROUP BY session_id HAVING COUNT(*) > 1`)
|
|
1017
|
+
.toArray()
|
|
1018
|
+
.map((raw) => {
|
|
1019
|
+
const decoded = decodeDamagedSession(raw);
|
|
1020
|
+
if (decoded._tag === "Failure")
|
|
1021
|
+
throw unavailable("Stored Session lineage audit is invalid");
|
|
1022
|
+
return decoded.success.session_id;
|
|
1023
|
+
});
|
|
1024
|
+
for (const sessionId of damaged) {
|
|
1025
|
+
owner.sql.exec("INSERT OR IGNORE INTO session_lineage_damage (session_id) VALUES (?)", sessionId);
|
|
1026
|
+
touchSessionInbox(owner, { sessionId, title: null });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// SQLite fires DELETE triggers for the row an INSERT OR REPLACE removes
|
|
1030
|
+
// only under PRAGMA recursive_triggers, which is off by default and which
|
|
1031
|
+
// Woodstock never turns on. So the delete guard cannot see a REPLACE; this
|
|
1032
|
+
// guard refuses a replacing row that would leave the replaced row's
|
|
1033
|
+
// children under a streaming, later or foreign parent.
|
|
1034
|
+
owner.sql.exec(`
|
|
1035
|
+
CREATE TRIGGER IF NOT EXISTS session_entries_lineage_replace
|
|
1036
|
+
BEFORE INSERT ON session_entries
|
|
1037
|
+
WHEN EXISTS (
|
|
1038
|
+
SELECT 1 FROM session_entries AS child
|
|
1039
|
+
WHERE child.parent_id = NEW.id
|
|
1040
|
+
AND (child.session_id != NEW.session_id
|
|
1041
|
+
OR child.sequence <= NEW.sequence
|
|
1042
|
+
OR NEW.state != 'complete'))
|
|
1043
|
+
BEGIN
|
|
1044
|
+
SELECT RAISE(ABORT, 'Session entry would strand its children');
|
|
1045
|
+
END
|
|
1046
|
+
`);
|
|
1047
|
+
// REPLACE also resolves the UNIQUE (session_id, sequence) conflict by
|
|
1048
|
+
// deleting whichever row holds that position, under any id. Refuse a
|
|
1049
|
+
// replacing row whose position belongs to another row with children.
|
|
1050
|
+
owner.sql.exec(`
|
|
1051
|
+
CREATE TRIGGER IF NOT EXISTS session_entries_lineage_replace_position
|
|
1052
|
+
BEFORE INSERT ON session_entries
|
|
1053
|
+
WHEN EXISTS (
|
|
1054
|
+
SELECT 1 FROM session_entries AS held
|
|
1055
|
+
JOIN session_entries AS child ON child.parent_id = held.id
|
|
1056
|
+
WHERE held.session_id = NEW.session_id
|
|
1057
|
+
AND held.sequence = NEW.sequence
|
|
1058
|
+
AND held.id != NEW.id)
|
|
1059
|
+
BEGIN
|
|
1060
|
+
SELECT RAISE(ABORT, 'Session entry would strand its children');
|
|
1061
|
+
END
|
|
1062
|
+
`);
|
|
1063
|
+
owner.sql.exec(`
|
|
1064
|
+
CREATE TRIGGER IF NOT EXISTS session_entries_damaged_session
|
|
1065
|
+
BEFORE INSERT ON session_entries
|
|
1066
|
+
WHEN EXISTS (
|
|
1067
|
+
SELECT 1 FROM session_lineage_damage WHERE session_id = NEW.session_id)
|
|
1068
|
+
BEGIN
|
|
1069
|
+
SELECT RAISE(ABORT, '${brokenSessionMessage}');
|
|
1070
|
+
END
|
|
1071
|
+
`);
|
|
1072
|
+
owner.sql.exec(`
|
|
1073
|
+
CREATE TRIGGER IF NOT EXISTS session_entries_lineage_insert
|
|
1074
|
+
BEFORE INSERT ON session_entries
|
|
1075
|
+
BEGIN
|
|
1076
|
+
SELECT RAISE(ABORT, 'Session entry parent is not an earlier complete entry of its Session')
|
|
1077
|
+
WHERE NEW.parent_id IS NOT NULL AND NOT EXISTS (
|
|
1078
|
+
SELECT 1 FROM session_entries AS parent
|
|
1079
|
+
WHERE parent.id = NEW.parent_id
|
|
1080
|
+
AND parent.session_id = NEW.session_id
|
|
1081
|
+
AND parent.state = 'complete'
|
|
1082
|
+
AND parent.sequence < NEW.sequence);
|
|
1083
|
+
SELECT RAISE(ABORT, 'Session already has a root entry')
|
|
1084
|
+
WHERE NEW.parent_id IS NULL AND EXISTS (
|
|
1085
|
+
SELECT 1 FROM session_entries AS other
|
|
1086
|
+
WHERE other.session_id = NEW.session_id
|
|
1087
|
+
AND other.state = 'complete'
|
|
1088
|
+
AND other.id != NEW.id);
|
|
1089
|
+
END
|
|
1090
|
+
`);
|
|
1091
|
+
owner.sql.exec(`
|
|
1092
|
+
CREATE TRIGGER IF NOT EXISTS session_entries_lineage_update
|
|
1093
|
+
BEFORE UPDATE OF id, session_id, parent_id, sequence, state
|
|
1094
|
+
ON session_entries
|
|
1095
|
+
BEGIN
|
|
1096
|
+
SELECT RAISE(ABORT, 'Session entry lineage columns are immutable');
|
|
1097
|
+
END
|
|
1098
|
+
`);
|
|
1099
|
+
owner.sql.exec(`
|
|
1100
|
+
CREATE TRIGGER IF NOT EXISTS session_entries_lineage_delete
|
|
1101
|
+
BEFORE DELETE ON session_entries
|
|
1102
|
+
WHEN EXISTS (SELECT 1 FROM sessions WHERE id = OLD.session_id)
|
|
1103
|
+
AND EXISTS (SELECT 1 FROM session_entries WHERE parent_id = OLD.id)
|
|
1104
|
+
BEGIN
|
|
1105
|
+
SELECT RAISE(ABORT, 'Session entry still has children');
|
|
1106
|
+
END
|
|
1107
|
+
`);
|
|
1108
|
+
}
|
|
823
1109
|
/** Create the retained Session schema and private creation/admission receipts. */
|
|
824
1110
|
export function prepareSessionStore(owner) {
|
|
825
1111
|
owner.transactionSync(() => {
|
|
@@ -853,10 +1139,26 @@ export function prepareSessionStore(owner) {
|
|
|
853
1139
|
ON session_entries(session_id, request_id)
|
|
854
1140
|
WHERE request_id IS NOT NULL`);
|
|
855
1141
|
owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_work ON session_entries(work_id, sequence)");
|
|
1142
|
+
// Context and preference reads look up these rare older facts directly
|
|
1143
|
+
// rather than walking the history behind them.
|
|
1144
|
+
owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_model_preference
|
|
1145
|
+
ON session_entries(session_id, sequence)
|
|
1146
|
+
WHERE type = 'model_preference'`);
|
|
1147
|
+
owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_cancellation_target
|
|
1148
|
+
ON session_entries(session_id, json_extract(data_json, '$.targetWorkId'))
|
|
1149
|
+
WHERE type = 'cancellation'`);
|
|
856
1150
|
owner.sql
|
|
857
1151
|
.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_successful_tool
|
|
858
1152
|
ON session_entries(session_id, work_id, json_extract(data_json, '$.call.id'))
|
|
859
1153
|
WHERE type = 'tool_outcome' AND json_extract(data_json, '$.result.isError') = 0`);
|
|
1154
|
+
guardSessionLineage(owner);
|
|
1155
|
+
owner.sql.exec(`
|
|
1156
|
+
CREATE TABLE IF NOT EXISTS session_request_withdrawals (
|
|
1157
|
+
session_id TEXT NOT NULL,
|
|
1158
|
+
request_id TEXT NOT NULL,
|
|
1159
|
+
PRIMARY KEY (session_id, request_id)
|
|
1160
|
+
)
|
|
1161
|
+
`);
|
|
860
1162
|
owner.sql.exec(`
|
|
861
1163
|
CREATE TABLE IF NOT EXISTS session_command_admissions (
|
|
862
1164
|
id TEXT PRIMARY KEY,
|
|
@@ -882,7 +1184,10 @@ export function prepareSessionStore(owner) {
|
|
|
882
1184
|
}
|
|
883
1185
|
/** Read the nullable preference at exactly this selected lineage. */
|
|
884
1186
|
export function readSessionModelPreference(owner, sessionId) {
|
|
885
|
-
|
|
1187
|
+
const leaf = readSessionRow(owner, sessionId)?.leaf_id;
|
|
1188
|
+
return leaf === undefined || leaf === null
|
|
1189
|
+
? null
|
|
1190
|
+
: lineageModelPreference(owner, sessionId, leaf);
|
|
886
1191
|
}
|
|
887
1192
|
/**
|
|
888
1193
|
* Woodstock order of the newest model choice on the selected lineage, or null.
|
|
@@ -892,26 +1197,7 @@ export function readLastModelChoiceSequence(owner, sessionId) {
|
|
|
892
1197
|
const leaf = readSessionRow(owner, sessionId)?.leaf_id;
|
|
893
1198
|
if (leaf === undefined || leaf === null)
|
|
894
1199
|
return null;
|
|
895
|
-
|
|
896
|
-
.exec(`WITH RECURSIVE branch (id, parent_id, sequence, type, state) AS (
|
|
897
|
-
SELECT id, parent_id, sequence, type, state
|
|
898
|
-
FROM session_entries
|
|
899
|
-
WHERE session_id = ? AND id = ?
|
|
900
|
-
UNION
|
|
901
|
-
SELECT parent.id, parent.parent_id, parent.sequence, parent.type,
|
|
902
|
-
parent.state
|
|
903
|
-
FROM session_entries AS parent
|
|
904
|
-
JOIN branch AS child ON parent.id = child.parent_id
|
|
905
|
-
WHERE parent.session_id = ?
|
|
906
|
-
)
|
|
907
|
-
SELECT MAX(sequence) AS sequence
|
|
908
|
-
FROM branch
|
|
909
|
-
WHERE type = 'model_preference' AND state != 'streaming'`, sessionId, leaf, sessionId)
|
|
910
|
-
.one();
|
|
911
|
-
const decoded = decodeModelChoiceRow(raw);
|
|
912
|
-
if (decoded._tag === "Failure")
|
|
913
|
-
throw unavailable("Stored model choice order is invalid");
|
|
914
|
-
return decoded.success.sequence;
|
|
1200
|
+
return newestLineagePreference(owner, sessionId, leaf)?.sequence ?? null;
|
|
915
1201
|
}
|
|
916
1202
|
/** Append a decoded preference request; no command or scheduler work is minted. */
|
|
917
1203
|
export function setSessionModelPreference(owner, sessionId, preference) {
|
|
@@ -929,7 +1215,12 @@ export function setSessionModelPreference(owner, sessionId, preference) {
|
|
|
929
1215
|
export function setSessionTitle(owner, sessionId, title) {
|
|
930
1216
|
owner.transactionSync(() => {
|
|
931
1217
|
ensureSession(owner, sessionId);
|
|
932
|
-
|
|
1218
|
+
const metadata = title === null ? null : encodeSessionMetadata({ title });
|
|
1219
|
+
if (owner.sql
|
|
1220
|
+
.exec("SELECT 1 FROM sessions WHERE id = ? AND metadata_json IS ?", sessionId, metadata)
|
|
1221
|
+
.toArray().length > 0)
|
|
1222
|
+
return;
|
|
1223
|
+
owner.sql.exec("UPDATE sessions SET metadata_json = ? WHERE id = ?", metadata, sessionId);
|
|
933
1224
|
touchSessionInbox(owner, {
|
|
934
1225
|
sessionId,
|
|
935
1226
|
title: inboxTitle(owner, sessionId),
|
|
@@ -1022,16 +1313,8 @@ export function completeSessionLeaf(owner, input) {
|
|
|
1022
1313
|
throw unavailable("Completed Session leaf is unavailable");
|
|
1023
1314
|
return completed;
|
|
1024
1315
|
}
|
|
1025
|
-
const entryColumns = `id, session_id, parent_id, sequence, type, role, state,
|
|
1026
|
-
request_id, work_id, data_json, timestamp, completed_at`;
|
|
1027
1316
|
/** Rows one bounded lineage read decodes at once. */
|
|
1028
1317
|
export const lineageRowLimit = 200;
|
|
1029
|
-
/**
|
|
1030
|
-
* Stored bytes one model context read may decode: the retained storage
|
|
1031
|
-
* plane's `LLM_CONTEXT_BYTES`, now a typed refusal rather than a silent
|
|
1032
|
-
* truncation, because a truncated context would change the model's input.
|
|
1033
|
-
*/
|
|
1034
|
-
export const contextByteLimit = 12 * 1_048_576;
|
|
1035
1318
|
/**
|
|
1036
1319
|
* Rows one fork copy window decodes. A fork copies the whole lineage, so a
|
|
1037
1320
|
* wide window keeps the walks few; the byte bound still caps each window.
|
|
@@ -1047,17 +1330,22 @@ const lineageByteLimit = 4 * 1_048_576;
|
|
|
1047
1330
|
* law); entry data is read afterwards for exactly the rows a reader needs.
|
|
1048
1331
|
* The selected walk drops streaming placeholders after walking; the fork walk
|
|
1049
1332
|
* stops at them, as a fork copies only complete history.
|
|
1333
|
+
* Each walk descends from the tip only while it is above a floor sequence,
|
|
1334
|
+
* so a reader of recent rows never visits the history behind them. The
|
|
1335
|
+
* selected walk steps only to an older parent, so it ends without
|
|
1336
|
+
* deduplicating; the fork walk keeps every link for its whole-lineage check.
|
|
1050
1337
|
*/
|
|
1051
1338
|
const lineageWalks = {
|
|
1052
1339
|
selected: `WITH RECURSIVE branch (id, parent_id, sequence, state) AS (
|
|
1053
1340
|
SELECT id, parent_id, sequence, state
|
|
1054
1341
|
FROM session_entries
|
|
1055
1342
|
WHERE session_id = ? AND id = ?
|
|
1056
|
-
UNION
|
|
1343
|
+
UNION ALL
|
|
1057
1344
|
SELECT parent.id, parent.parent_id, parent.sequence, parent.state
|
|
1058
1345
|
FROM session_entries AS parent
|
|
1059
1346
|
JOIN branch AS child ON parent.id = child.parent_id
|
|
1060
|
-
WHERE parent.session_id = ?
|
|
1347
|
+
WHERE parent.session_id = ? AND child.sequence > ?
|
|
1348
|
+
AND +parent.sequence < child.sequence
|
|
1061
1349
|
)`,
|
|
1062
1350
|
fork: `WITH RECURSIVE branch (id, parent_id, sequence, state) AS (
|
|
1063
1351
|
SELECT id, parent_id, sequence, state
|
|
@@ -1067,7 +1355,8 @@ const lineageWalks = {
|
|
|
1067
1355
|
SELECT parent.id, parent.parent_id, parent.sequence, parent.state
|
|
1068
1356
|
FROM session_entries AS parent
|
|
1069
1357
|
JOIN branch AS child ON parent.id = child.parent_id
|
|
1070
|
-
WHERE parent.session_id = ? AND
|
|
1358
|
+
WHERE parent.session_id = ? AND child.sequence > ?
|
|
1359
|
+
AND parent.state != 'streaming'
|
|
1071
1360
|
)`,
|
|
1072
1361
|
};
|
|
1073
1362
|
const lineageFailures = {
|
|
@@ -1091,18 +1380,23 @@ const decodeLineageCheck = Schema.decodeUnknownResult(Schema.Struct({
|
|
|
1091
1380
|
newest_id: Schema.NullOr(boundedId),
|
|
1092
1381
|
newest_sequence: Schema.NullOr(sequence),
|
|
1093
1382
|
}), closed);
|
|
1094
|
-
const decodeLineageSize = Schema.decodeUnknownResult(Schema.Struct({ sequence, bytes: count }), closed);
|
|
1095
|
-
const decodeContextBytes = Schema.decodeUnknownResult(Schema.Struct({ bytes: count }), closed);
|
|
1096
1383
|
const decodeLineageSequence = Schema.decodeUnknownResult(Schema.Struct({ sequence }), closed);
|
|
1097
|
-
const decodeFirstKept = Schema.decodeUnknownResult(Schema.Struct({ first_kept: boundedId }), closed);
|
|
1098
1384
|
const decodeLineageWorkId = Schema.decodeUnknownResult(Schema.Struct({ work_id: boundedId }), closed);
|
|
1099
1385
|
const decodeLineageDiagnostic = Schema.decodeUnknownResult(Schema.Struct({ work_id: boundedId, diagnostic: Schema.String }), closed);
|
|
1100
|
-
|
|
1386
|
+
/**
|
|
1387
|
+
* Run one query over the lineage rows above `floor`: the walk descends from
|
|
1388
|
+
* the tip while its rows are above it, so only those rows, and the first at
|
|
1389
|
+
* or below it, are visited.
|
|
1390
|
+
*/
|
|
1391
|
+
function lineageQuery(owner, lineage, floor, query, ...bindings) {
|
|
1101
1392
|
return owner.sql
|
|
1102
|
-
.exec(`${lineageWalks[lineage.walk]} ${query}`, lineage.sessionId, lineage.tipId, lineage.sessionId, ...bindings)
|
|
1393
|
+
.exec(`${lineageWalks[lineage.walk]} ${query}`, lineage.sessionId, lineage.tipId, lineage.sessionId, floor, ...bindings)
|
|
1103
1394
|
.toArray();
|
|
1104
1395
|
}
|
|
1105
|
-
/**
|
|
1396
|
+
/**
|
|
1397
|
+
* Walk and check one whole root-to-tip chain in SQL, the same way decoded
|
|
1398
|
+
* rows would be. A fork copies every row of it, so it checks every row.
|
|
1399
|
+
*/
|
|
1106
1400
|
function locateLineage(owner, sessionId, tipId, walk) {
|
|
1107
1401
|
const failures = lineageFailures[walk];
|
|
1108
1402
|
const checked = decodeLineageCheck(owner.sql
|
|
@@ -1124,7 +1418,7 @@ function locateLineage(owner, sessionId, tipId, walk) {
|
|
|
1124
1418
|
WHERE previous IS NOT NULL AND parent_id IS NOT previous) AS broken,
|
|
1125
1419
|
(SELECT id FROM kept ORDER BY sequence DESC LIMIT 1) AS newest_id,
|
|
1126
1420
|
(SELECT sequence FROM kept ORDER BY sequence DESC LIMIT 1)
|
|
1127
|
-
AS newest_sequence`, sessionId, tipId, sessionId)
|
|
1421
|
+
AS newest_sequence`, sessionId, tipId, sessionId, 0)
|
|
1128
1422
|
.one());
|
|
1129
1423
|
if (checked._tag === "Failure")
|
|
1130
1424
|
throw unavailable("Stored Session lineage check is invalid");
|
|
@@ -1147,38 +1441,147 @@ function locateLineage(owner, sessionId, tipId, walk) {
|
|
|
1147
1441
|
newestSequence: lineage.newest_sequence,
|
|
1148
1442
|
});
|
|
1149
1443
|
}
|
|
1150
|
-
|
|
1444
|
+
const decodeLineageEnds = Schema.decodeUnknownResult(Schema.Struct({
|
|
1445
|
+
state: Schema.Literals(["complete", "streaming"]),
|
|
1446
|
+
newest_sequence: sequence,
|
|
1447
|
+
root_id: Schema.NullOr(boundedId),
|
|
1448
|
+
root_parent_id: Schema.NullOr(Schema.String),
|
|
1449
|
+
}), closed);
|
|
1450
|
+
/**
|
|
1451
|
+
* Locate a selected lineage from its two ends; undefined when the Session
|
|
1452
|
+
* has no leaf. The guards make a Session's one complete root the root of
|
|
1453
|
+
* every lineage in it, so neither end needs a walk.
|
|
1454
|
+
*/
|
|
1151
1455
|
export function readSessionLineage(owner, input) {
|
|
1152
|
-
|
|
1456
|
+
// Read the Session row even for an explicit tip: it refuses a broken one.
|
|
1457
|
+
const session = readSessionRow(owner, input.sessionId);
|
|
1458
|
+
const tip = input.leafId ?? session?.leaf_id;
|
|
1153
1459
|
if (tip === undefined || tip === null)
|
|
1154
1460
|
return undefined;
|
|
1155
|
-
|
|
1461
|
+
const failures = lineageFailures.selected;
|
|
1462
|
+
const raw = owner.sql
|
|
1463
|
+
.exec(`SELECT tip.state AS state, tip.sequence AS newest_sequence,
|
|
1464
|
+
root.id AS root_id, root.parent_id AS root_parent_id
|
|
1465
|
+
FROM session_entries AS tip
|
|
1466
|
+
LEFT JOIN (
|
|
1467
|
+
SELECT id, parent_id FROM session_entries
|
|
1468
|
+
WHERE session_id = ? AND state = 'complete'
|
|
1469
|
+
ORDER BY sequence ASC LIMIT 1
|
|
1470
|
+
) AS root
|
|
1471
|
+
WHERE tip.session_id = ? AND tip.id = ?`, input.sessionId, input.sessionId, tip)
|
|
1472
|
+
.toArray()[0];
|
|
1473
|
+
if (raw === undefined)
|
|
1474
|
+
throw unavailable(failures.empty);
|
|
1475
|
+
const ends = decodeLineageEnds(raw);
|
|
1476
|
+
if (ends._tag === "Failure")
|
|
1477
|
+
throw unavailable("Stored Session lineage ends are invalid");
|
|
1478
|
+
if (ends.success.state !== "complete" || ends.success.root_id === null)
|
|
1479
|
+
throw unavailable(failures.empty);
|
|
1480
|
+
if (ends.success.root_parent_id !== null)
|
|
1481
|
+
throw unavailable(failures.missingParent);
|
|
1482
|
+
return Object.freeze({
|
|
1483
|
+
sessionId: input.sessionId,
|
|
1484
|
+
walk: "selected",
|
|
1485
|
+
tipId: tip,
|
|
1486
|
+
rootId: ends.success.root_id,
|
|
1487
|
+
newestId: tip,
|
|
1488
|
+
newestSequence: ends.success.newest_sequence,
|
|
1489
|
+
});
|
|
1156
1490
|
}
|
|
1157
1491
|
function readLineageRange(owner, lineage, oldest, newest) {
|
|
1158
|
-
|
|
1492
|
+
const rows = Object.freeze(lineageQuery(owner, lineage, oldest - 1, `SELECT ${entryColumns}
|
|
1159
1493
|
FROM session_entries
|
|
1160
1494
|
WHERE session_id = ? AND state != 'streaming'
|
|
1161
1495
|
AND sequence BETWEEN ? AND ?
|
|
1162
1496
|
AND id IN (SELECT id FROM branch)
|
|
1163
|
-
ORDER BY sequence ASC`, lineage.sessionId, oldest, newest).map(parseEntryRow));
|
|
1497
|
+
ORDER BY sequence ASC`, lineage.sessionId, oldest, newest).map((raw) => parseEntryRow(owner, raw)));
|
|
1498
|
+
// The walk stopped at the range or at the root, never at a missing parent.
|
|
1499
|
+
const failures = lineageFailures.selected;
|
|
1500
|
+
const deepest = rows[0];
|
|
1501
|
+
if (deepest !== undefined &&
|
|
1502
|
+
deepest.parentId !== null &&
|
|
1503
|
+
deepest.sequence !== oldest)
|
|
1504
|
+
throw unavailable(failures.missingParent);
|
|
1505
|
+
let previous;
|
|
1506
|
+
for (const row of rows) {
|
|
1507
|
+
if (previous !== undefined && row.parentId !== previous.id)
|
|
1508
|
+
throw unavailable(failures.invalid);
|
|
1509
|
+
previous = row;
|
|
1510
|
+
}
|
|
1511
|
+
return rows;
|
|
1512
|
+
}
|
|
1513
|
+
const decodeWindowSizes = Schema.decodeUnknownResult(Schema.Struct({
|
|
1514
|
+
sizes: Schema.fromJsonString(Schema.Array(Schema.Tuple([sequence, count]))),
|
|
1515
|
+
deepest_parent: Schema.NullOr(Schema.String),
|
|
1516
|
+
deepest_sequence: Schema.NullOr(sequence),
|
|
1517
|
+
}), closed);
|
|
1518
|
+
/**
|
|
1519
|
+
* Sizes of up to `rows` lineage rows inside the bounds, newest or oldest
|
|
1520
|
+
* first. A newest window's walk counts its rows as it descends and stops once
|
|
1521
|
+
* it holds that many; an oldest window's walk stops at `after`. Either way the
|
|
1522
|
+
* walk visits the rows from the tip down to the window, never the history
|
|
1523
|
+
* below it. The deepest walked row comes back with the sizes, so a walk that
|
|
1524
|
+
* ended at a missing parent above where it meant to stop is refused, never
|
|
1525
|
+
* read as the end of history.
|
|
1526
|
+
*/
|
|
1527
|
+
function readWindowSizes(owner, lineage, from, after, before, rows) {
|
|
1528
|
+
const ends = `SELECT
|
|
1529
|
+
(SELECT json_group_array(json_array(sequence, bytes)) FROM (
|
|
1530
|
+
SELECT sequence, LENGTH(CAST(data_json AS BLOB)) AS bytes
|
|
1531
|
+
FROM session_entries
|
|
1532
|
+
WHERE ${from === "newest" ? "" : "session_id = ? AND"}
|
|
1533
|
+
state != 'streaming'
|
|
1534
|
+
AND sequence > ? AND sequence < ?
|
|
1535
|
+
AND id IN (SELECT id FROM branch)
|
|
1536
|
+
ORDER BY sequence ${from === "newest" ? "DESC" : "ASC"}
|
|
1537
|
+
LIMIT ?)) AS sizes,
|
|
1538
|
+
(SELECT parent_id FROM branch ORDER BY sequence ASC LIMIT 1)
|
|
1539
|
+
AS deepest_parent,
|
|
1540
|
+
(SELECT sequence FROM branch ORDER BY sequence ASC LIMIT 1)
|
|
1541
|
+
AS deepest_sequence`;
|
|
1542
|
+
const counts = (row) => `CASE WHEN ${row}.state != 'streaming'
|
|
1543
|
+
AND ${row}.sequence < ? AND ${row}.sequence > ?
|
|
1544
|
+
THEN 1 ELSE 0 END`;
|
|
1545
|
+
const raw = from === "newest"
|
|
1546
|
+
? owner.sql
|
|
1547
|
+
.exec(`WITH RECURSIVE branch (id, parent_id, sequence, state, taken) AS (
|
|
1548
|
+
SELECT id, parent_id, sequence, state,
|
|
1549
|
+
${counts("session_entries")}
|
|
1550
|
+
FROM session_entries
|
|
1551
|
+
WHERE session_id = ? AND id = ?
|
|
1552
|
+
UNION ALL
|
|
1553
|
+
SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
|
|
1554
|
+
child.taken + ${counts("parent")}
|
|
1555
|
+
FROM session_entries AS parent
|
|
1556
|
+
JOIN branch AS child ON parent.id = child.parent_id
|
|
1557
|
+
WHERE parent.session_id = ? AND +parent.sequence < child.sequence
|
|
1558
|
+
AND child.taken < ? AND child.sequence > ?
|
|
1559
|
+
)
|
|
1560
|
+
${ends}`, before, after, lineage.sessionId, lineage.tipId, before, after, lineage.sessionId, rows, after, after, before, rows)
|
|
1561
|
+
.toArray()
|
|
1562
|
+
: lineageQuery(owner, lineage, after, ends, lineage.sessionId, after, before, rows);
|
|
1563
|
+
const decoded = decodeWindowSizes(raw[0]);
|
|
1564
|
+
if (decoded._tag === "Failure")
|
|
1565
|
+
throw unavailable("Stored Session entry size is invalid");
|
|
1566
|
+
const { sizes, deepest_parent, deepest_sequence } = decoded.success;
|
|
1567
|
+
const stopped = deepest_parent === null ||
|
|
1568
|
+
deepest_sequence === null ||
|
|
1569
|
+
deepest_sequence <= after ||
|
|
1570
|
+
(from === "newest" && sizes.length === rows);
|
|
1571
|
+
if (!stopped)
|
|
1572
|
+
throw unavailable(lineageFailures.selected.missingParent);
|
|
1573
|
+
return Object.freeze(sizes
|
|
1574
|
+
.map(([rowSequence, bytes]) => ({ sequence: rowSequence, bytes }))
|
|
1575
|
+
.sort((left, right) => from === "newest"
|
|
1576
|
+
? right.sequence - left.sequence
|
|
1577
|
+
: left.sequence - right.sequence));
|
|
1164
1578
|
}
|
|
1165
1579
|
/**
|
|
1166
1580
|
* Read one contiguous window of a lineage, bounded by row count and by stored
|
|
1167
1581
|
* bytes. Row sizes are measured in SQL before any entry is decoded.
|
|
1168
1582
|
*/
|
|
1169
1583
|
export function readLineageWindow(owner, lineage, input) {
|
|
1170
|
-
const sizes =
|
|
1171
|
-
FROM session_entries
|
|
1172
|
-
WHERE session_id = ? AND state != 'streaming'
|
|
1173
|
-
AND sequence > ? AND sequence < ?
|
|
1174
|
-
AND id IN (SELECT id FROM branch)
|
|
1175
|
-
ORDER BY sequence ${input.from === "newest" ? "DESC" : "ASC"}
|
|
1176
|
-
LIMIT ?`, lineage.sessionId, input.after ?? 0, input.before ?? Number.MAX_SAFE_INTEGER, input.rows + 1).map((raw) => {
|
|
1177
|
-
const decoded = decodeLineageSize(raw);
|
|
1178
|
-
if (decoded._tag === "Failure")
|
|
1179
|
-
throw unavailable("Stored Session entry size is invalid");
|
|
1180
|
-
return decoded.success;
|
|
1181
|
-
});
|
|
1584
|
+
const sizes = readWindowSizes(owner, lineage, input.from, input.after ?? 0, input.before ?? Number.MAX_SAFE_INTEGER, input.rows + 1);
|
|
1182
1585
|
let taken = 0;
|
|
1183
1586
|
let bytes = 0;
|
|
1184
1587
|
for (const size of sizes) {
|
|
@@ -1200,45 +1603,59 @@ export function readLineageWindow(owner, lineage, input) {
|
|
|
1200
1603
|
}
|
|
1201
1604
|
/** Whether a complete row with this sequence lies on the lineage. */
|
|
1202
1605
|
export function lineageHasSequence(owner, lineage, rowSequence) {
|
|
1203
|
-
return (lineageQuery(owner, lineage, `SELECT 1 AS present
|
|
1606
|
+
return (lineageQuery(owner, lineage, rowSequence - 1, `SELECT 1 AS present
|
|
1204
1607
|
FROM session_entries
|
|
1205
1608
|
WHERE session_id = ? AND state != 'streaming' AND sequence = ?
|
|
1206
1609
|
AND id IN (SELECT id FROM branch)`, lineage.sessionId, rowSequence).length > 0);
|
|
1207
1610
|
}
|
|
1208
1611
|
/** The sequence of one complete row on the lineage, if it is there. */
|
|
1209
1612
|
export function lineageEntrySequence(owner, lineage, id) {
|
|
1210
|
-
const raw =
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1613
|
+
const raw = owner.sql
|
|
1614
|
+
.exec(`SELECT sequence FROM session_entries
|
|
1615
|
+
WHERE session_id = ? AND id = ? AND state != 'streaming'`, lineage.sessionId, id)
|
|
1616
|
+
.toArray()[0];
|
|
1214
1617
|
if (raw === undefined)
|
|
1215
1618
|
return undefined;
|
|
1216
1619
|
const decoded = decodeLineageSequence(raw);
|
|
1217
1620
|
if (decoded._tag === "Failure")
|
|
1218
1621
|
throw unavailable("Stored Session entry sequence is invalid");
|
|
1219
|
-
return decoded.success.sequence
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
function readNewestLineageEntry(owner, lineage, kind) {
|
|
1223
|
-
const raw = lineageQuery(owner, lineage, `SELECT ${entryColumns}
|
|
1224
|
-
FROM session_entries
|
|
1225
|
-
WHERE session_id = ? AND state != 'streaming' AND type = ?
|
|
1226
|
-
AND id IN (SELECT id FROM branch)
|
|
1227
|
-
ORDER BY sequence DESC
|
|
1228
|
-
LIMIT 1`, lineage.sessionId, kind)[0];
|
|
1229
|
-
return raw === undefined ? undefined : parseEntryRow(raw);
|
|
1622
|
+
return lineageHasSequence(owner, lineage, decoded.success.sequence)
|
|
1623
|
+
? decoded.success.sequence
|
|
1624
|
+
: undefined;
|
|
1230
1625
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1626
|
+
/**
|
|
1627
|
+
* The newest preference on the lineage ending at `tipId`. The Session's
|
|
1628
|
+
* preference rows come from their index; the walk from the tip stops at the
|
|
1629
|
+
* first of them it meets, so it never passes the newest preference on this
|
|
1630
|
+
* lineage, and a Session without one walks nothing. One set at the root
|
|
1631
|
+
* still walks the whole lineage.
|
|
1632
|
+
*/
|
|
1633
|
+
function newestLineagePreference(owner, sessionId, tipId) {
|
|
1634
|
+
const continues = (row) => `${row}.id NOT IN (SELECT id FROM targets)
|
|
1635
|
+
AND ${row}.sequence > (SELECT MIN(sequence) FROM targets)`;
|
|
1636
|
+
const [newest] = walkedTargets(owner.sql
|
|
1637
|
+
.exec(`WITH RECURSIVE targets AS MATERIALIZED (
|
|
1638
|
+
SELECT id, sequence FROM session_entries
|
|
1639
|
+
WHERE session_id = ? AND type = 'model_preference'
|
|
1640
|
+
AND state != 'streaming'
|
|
1641
|
+
),
|
|
1642
|
+
${targetWalk(continues("child"))}
|
|
1643
|
+
${walkSteps(continues("walk"))}`, sessionId, sessionId, tipId, sessionId)
|
|
1644
|
+
.toArray());
|
|
1645
|
+
return newest === undefined
|
|
1233
1646
|
? undefined
|
|
1234
|
-
:
|
|
1647
|
+
: readEntryRow(owner, sessionId, newest);
|
|
1648
|
+
}
|
|
1649
|
+
function lineageModelPreference(owner, sessionId, tipId) {
|
|
1650
|
+
const entry = newestLineagePreference(owner, sessionId, tipId)?.entry;
|
|
1235
1651
|
return entry?.kind === "model_preference" ? entry.preference : null;
|
|
1236
1652
|
}
|
|
1237
1653
|
/** Latest document value on this exact lineage, not a global last write. */
|
|
1238
1654
|
function readLineageDocument(owner, lineage, extensionId, name) {
|
|
1239
1655
|
if (lineage === undefined)
|
|
1240
1656
|
return undefined;
|
|
1241
|
-
|
|
1657
|
+
// No index finds documents yet, so this still walks the whole lineage.
|
|
1658
|
+
const raw = lineageQuery(owner, lineage, 0, `SELECT ${entryColumns}
|
|
1242
1659
|
FROM session_entries
|
|
1243
1660
|
WHERE session_id = ? AND state != 'streaming' AND type = 'document'
|
|
1244
1661
|
AND json_extract(data_json, '$.extensionId') = ?
|
|
@@ -1246,7 +1663,7 @@ function readLineageDocument(owner, lineage, extensionId, name) {
|
|
|
1246
1663
|
AND id IN (SELECT id FROM branch)
|
|
1247
1664
|
ORDER BY sequence DESC
|
|
1248
1665
|
LIMIT 1`, lineage.sessionId, extensionId, name)[0];
|
|
1249
|
-
const entry = raw === undefined ? undefined : parseEntryRow(raw).entry;
|
|
1666
|
+
const entry = raw === undefined ? undefined : parseEntryRow(owner, raw).entry;
|
|
1250
1667
|
if (entry === undefined)
|
|
1251
1668
|
return undefined;
|
|
1252
1669
|
if (entry.kind !== "document" ||
|
|
@@ -1255,10 +1672,12 @@ function readLineageDocument(owner, lineage, extensionId, name) {
|
|
|
1255
1672
|
throw unavailable("Stored Session document disagrees with its row");
|
|
1256
1673
|
return entry.valueJson;
|
|
1257
1674
|
}
|
|
1675
|
+
const decodeOldestWorkRow = Schema.decodeUnknownResult(Schema.Struct({ sequence: Schema.NullOr(sequence) }), closed);
|
|
1258
1676
|
/**
|
|
1259
|
-
* Look up,
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1677
|
+
* Look up, on the lineage, only the facts of the given works. Diagnostics come
|
|
1678
|
+
* only from owned terminal fields, never provider message or retry text. The works'
|
|
1679
|
+
* indexes give their oldest row, and the walks stop just below it, so a page
|
|
1680
|
+
* of recent works never visits older history.
|
|
1262
1681
|
*/
|
|
1263
1682
|
export function readLineageWorkFacts(owner, lineage, workIds) {
|
|
1264
1683
|
if (workIds.size === 0)
|
|
@@ -1268,49 +1687,64 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
|
|
|
1268
1687
|
cancellations: new Map(),
|
|
1269
1688
|
});
|
|
1270
1689
|
const works = JSON.stringify([...workIds]);
|
|
1271
|
-
const
|
|
1690
|
+
const oldest = decodeOldestWorkRow(owner.sql
|
|
1691
|
+
.exec(`SELECT MIN(sequence) AS sequence FROM (
|
|
1692
|
+
SELECT MIN(sequence) AS sequence
|
|
1693
|
+
FROM session_entries INDEXED BY idx_session_entries_work
|
|
1694
|
+
WHERE work_id IN (SELECT value FROM json_each(?))
|
|
1695
|
+
AND session_id = ?
|
|
1696
|
+
UNION ALL
|
|
1697
|
+
SELECT MIN(sequence) FROM session_entries
|
|
1698
|
+
INDEXED BY idx_session_entries_cancellation_target
|
|
1699
|
+
WHERE session_id = ? AND type = 'cancellation'
|
|
1700
|
+
AND json_extract(data_json, '$.targetWorkId')
|
|
1701
|
+
IN (SELECT value FROM json_each(?))
|
|
1702
|
+
)`, works, lineage.sessionId, lineage.sessionId, works)
|
|
1703
|
+
.one());
|
|
1704
|
+
if (oldest._tag === "Failure")
|
|
1705
|
+
throw unavailable("Stored Session work order is invalid");
|
|
1706
|
+
if (oldest.success.sequence === null)
|
|
1707
|
+
return Object.freeze({
|
|
1708
|
+
commands: new Set(),
|
|
1709
|
+
diagnostics: new Map(),
|
|
1710
|
+
cancellations: new Map(),
|
|
1711
|
+
});
|
|
1712
|
+
const floor = oldest.success.sequence - 1;
|
|
1713
|
+
const commands = new Set(lineageQuery(owner, lineage, floor, `SELECT DISTINCT work_id
|
|
1272
1714
|
FROM session_entries
|
|
1273
|
-
WHERE session_id = ? AND state != 'streaming' AND type = 'command'
|
|
1715
|
+
WHERE session_id = ? AND sequence > ? AND state != 'streaming' AND type = 'command'
|
|
1274
1716
|
AND work_id IN (SELECT value FROM json_each(?))
|
|
1275
|
-
AND id IN (SELECT id FROM branch)`, lineage.sessionId, works).map((raw) => {
|
|
1717
|
+
AND id IN (SELECT id FROM branch)`, lineage.sessionId, floor, works).map((raw) => {
|
|
1276
1718
|
const decoded = decodeLineageWorkId(raw);
|
|
1277
1719
|
if (decoded._tag === "Failure")
|
|
1278
1720
|
throw unavailable("Stored Session command work is invalid");
|
|
1279
1721
|
return decoded.success.work_id;
|
|
1280
1722
|
}));
|
|
1281
1723
|
const diagnostics = new Map();
|
|
1282
|
-
for (const raw of lineageQuery(owner, lineage, `SELECT work_id, diagnostic FROM (
|
|
1724
|
+
for (const raw of lineageQuery(owner, lineage, floor, `SELECT work_id, diagnostic FROM (
|
|
1283
1725
|
SELECT work_id, sequence,
|
|
1284
|
-
|
|
1285
|
-
WHEN type = 'work_terminal'
|
|
1286
|
-
THEN json_extract(data_json, '$.diagnostic')
|
|
1287
|
-
WHEN type = 'message'
|
|
1288
|
-
THEN json_extract(data_json, '$.value.errorMessage')
|
|
1289
|
-
WHEN json_extract(data_json, '$.event.type') = 'auto_retry_end'
|
|
1290
|
-
THEN json_extract(data_json, '$.event.finalError')
|
|
1291
|
-
END AS diagnostic
|
|
1726
|
+
json_extract(data_json, '$.diagnostic') AS diagnostic
|
|
1292
1727
|
FROM session_entries
|
|
1293
|
-
WHERE session_id = ? AND state != 'streaming'
|
|
1728
|
+
WHERE session_id = ? AND sequence > ? AND state != 'streaming'
|
|
1294
1729
|
AND work_id IN (SELECT value FROM json_each(?))
|
|
1295
|
-
AND
|
|
1296
|
-
OR (type = 'message' AND role = 'assistant'))
|
|
1730
|
+
AND type IN ('work_terminal', 'run_terminal')
|
|
1297
1731
|
AND id IN (SELECT id FROM branch)
|
|
1298
1732
|
)
|
|
1299
1733
|
WHERE diagnostic IS NOT NULL
|
|
1300
|
-
ORDER BY sequence ASC`, lineage.sessionId, works)) {
|
|
1734
|
+
ORDER BY sequence ASC`, lineage.sessionId, floor, works)) {
|
|
1301
1735
|
const decoded = decodeLineageDiagnostic(raw);
|
|
1302
1736
|
if (decoded._tag === "Failure")
|
|
1303
1737
|
throw unavailable("Stored Session diagnostic is invalid");
|
|
1304
1738
|
diagnostics.set(decoded.success.work_id, decoded.success.diagnostic);
|
|
1305
1739
|
}
|
|
1306
1740
|
const cancellations = new Map();
|
|
1307
|
-
for (const { entry } of lineageQuery(owner, lineage, `SELECT ${entryColumns}
|
|
1741
|
+
for (const { entry } of lineageQuery(owner, lineage, floor, `SELECT ${entryColumns}
|
|
1308
1742
|
FROM session_entries
|
|
1309
|
-
WHERE session_id = ? AND state != 'streaming' AND type = 'cancellation'
|
|
1743
|
+
WHERE session_id = ? AND sequence > ? AND state != 'streaming' AND type = 'cancellation'
|
|
1310
1744
|
AND json_extract(data_json, '$.targetWorkId')
|
|
1311
1745
|
IN (SELECT value FROM json_each(?))
|
|
1312
1746
|
AND id IN (SELECT id FROM branch)
|
|
1313
|
-
ORDER BY sequence ASC`, lineage.sessionId, works).map(parseEntryRow))
|
|
1747
|
+
ORDER BY sequence ASC`, lineage.sessionId, floor, works).map((raw) => parseEntryRow(owner, raw)))
|
|
1314
1748
|
if (entry.kind === "cancellation" && entry.meta !== undefined)
|
|
1315
1749
|
cancellations.set(entry.targetWorkId, entry.meta);
|
|
1316
1750
|
return Object.freeze({ commands, diagnostics, cancellations });
|
|
@@ -1327,81 +1761,245 @@ export function readSelectedPath(owner, input) {
|
|
|
1327
1761
|
? Object.freeze([])
|
|
1328
1762
|
: readLineageRange(owner, lineage, 1, lineage.newestSequence);
|
|
1329
1763
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1764
|
+
/**
|
|
1765
|
+
* A walk from one row toward the root for the `targets` the statement
|
|
1766
|
+
* selects first by index. `continues` decides, per row, whether the walk
|
|
1767
|
+
* steps past it; a statement without targets reads only its starting row,
|
|
1768
|
+
* so a missing one is still refused.
|
|
1769
|
+
*/
|
|
1770
|
+
function targetWalk(continues) {
|
|
1771
|
+
return `walk (id, parent_id, sequence, state) AS (
|
|
1772
|
+
SELECT id, parent_id, sequence, state
|
|
1773
|
+
FROM session_entries
|
|
1774
|
+
WHERE session_id = ? AND id = ?
|
|
1775
|
+
UNION ALL
|
|
1776
|
+
SELECT parent.id, parent.parent_id, parent.sequence, parent.state
|
|
1777
|
+
FROM session_entries AS parent
|
|
1778
|
+
JOIN walk AS child ON parent.id = child.parent_id
|
|
1779
|
+
WHERE parent.session_id = ? AND +parent.sequence < child.sequence
|
|
1780
|
+
AND ${continues}
|
|
1781
|
+
)`;
|
|
1340
1782
|
}
|
|
1341
1783
|
/**
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
* segments, where `buildSessionContext` also reads the whole path.
|
|
1784
|
+
* The walked targets, newest first, then the deepest walked row, each with
|
|
1785
|
+
* whether the walk meant to pass it. Only these rows reach JavaScript.
|
|
1345
1786
|
*/
|
|
1346
|
-
function
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1787
|
+
function walkSteps(continues) {
|
|
1788
|
+
return `SELECT id, parent_id, target, continues FROM (
|
|
1789
|
+
SELECT id, parent_id, sequence,
|
|
1790
|
+
id IN (SELECT id FROM targets) AS target,
|
|
1791
|
+
COALESCE((${continues}), 0) AS continues
|
|
1792
|
+
FROM walk
|
|
1793
|
+
)
|
|
1794
|
+
WHERE target = 1 OR sequence = (SELECT MIN(sequence) FROM walk)
|
|
1795
|
+
ORDER BY sequence DESC`;
|
|
1796
|
+
}
|
|
1797
|
+
const flag = Schema.Literals([0, 1]);
|
|
1798
|
+
const decodeWalkStep = Schema.decodeUnknownResult(Schema.Struct({
|
|
1799
|
+
id: boundedId,
|
|
1800
|
+
parent_id: Schema.NullOr(Schema.String),
|
|
1801
|
+
target: flag,
|
|
1802
|
+
continues: flag,
|
|
1803
|
+
}), closed);
|
|
1804
|
+
/**
|
|
1805
|
+
* The targets a bounded walk reached, newest first, once the walk is known to
|
|
1806
|
+
* have ended at the root or where it meant to stop, never at a missing or
|
|
1807
|
+
* out-of-order parent. Walks step only to an older parent, so they end.
|
|
1808
|
+
* A walk that found not even its starting row is refused as an unavailable
|
|
1809
|
+
* leaf.
|
|
1810
|
+
*/
|
|
1811
|
+
function walkedTargets(raws) {
|
|
1812
|
+
const failures = lineageFailures.selected;
|
|
1813
|
+
if (raws.length === 0)
|
|
1814
|
+
throw unavailable(failures.empty);
|
|
1815
|
+
const steps = raws.map((raw) => {
|
|
1816
|
+
const decoded = decodeWalkStep(raw);
|
|
1817
|
+
if (decoded._tag === "Failure")
|
|
1818
|
+
throw unavailable("Stored Session walk row is invalid");
|
|
1819
|
+
return decoded.success;
|
|
1820
|
+
});
|
|
1821
|
+
const deepest = steps.at(-1);
|
|
1822
|
+
if (deepest?.continues === 1 && deepest.parent_id !== null)
|
|
1823
|
+
throw unavailable(failures.missingParent);
|
|
1824
|
+
return steps.flatMap((step) => (step.target === 1 ? [step.id] : []));
|
|
1825
|
+
}
|
|
1826
|
+
/** The kept head of a complete compaction row, else null. */
|
|
1827
|
+
function compactionHead(row) {
|
|
1828
|
+
return `CASE WHEN ${row}.type = 'compaction' AND ${row}.state != 'streaming'
|
|
1829
|
+
THEN json_extract(${row}.data_json, '$.head') END`;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* The walk a settled context reads: from the tip back to the kept entry of
|
|
1833
|
+
* the newest complete compaction, which `head` carries once passed, or to the
|
|
1834
|
+
* root when nothing compacted or that entry is not on this lineage. It never
|
|
1835
|
+
* visits the history a compaction already cut.
|
|
1836
|
+
*/
|
|
1837
|
+
const contextWalk = `WITH RECURSIVE walk (id, parent_id, sequence, state, head) AS (
|
|
1838
|
+
SELECT id, parent_id, sequence, state,
|
|
1839
|
+
${compactionHead("session_entries")}
|
|
1840
|
+
FROM session_entries
|
|
1841
|
+
WHERE session_id = ? AND id = ?
|
|
1842
|
+
UNION ALL
|
|
1843
|
+
SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
|
|
1844
|
+
COALESCE(child.head, ${compactionHead("parent")})
|
|
1845
|
+
FROM session_entries AS parent
|
|
1846
|
+
JOIN walk AS child ON parent.id = child.parent_id
|
|
1847
|
+
WHERE parent.session_id = ? AND +parent.sequence < child.sequence
|
|
1848
|
+
AND (child.head IS NOT child.id OR child.state = 'streaming')
|
|
1849
|
+
)`;
|
|
1850
|
+
/**
|
|
1851
|
+
* The older facts that still decide a kept window: compactions of the active
|
|
1852
|
+
* work and cancellations of a command inside the window. Their indexes find
|
|
1853
|
+
* them; the walk down from the kept entry stops at the oldest of them, so
|
|
1854
|
+
* only facts on this lineage count and a window without any walks nothing.
|
|
1855
|
+
*/
|
|
1856
|
+
function readDecidingFacts(owner, sessionId, kept, window, workId) {
|
|
1857
|
+
const commands = JSON.stringify(window.flatMap(({ entry }) => entry.kind === "command" ? [entry.workId] : []));
|
|
1858
|
+
const continues = (row) => `${row}.sequence > (SELECT MIN(sequence) FROM targets)`;
|
|
1859
|
+
const facts = walkedTargets(owner.sql
|
|
1860
|
+
.exec(`WITH RECURSIVE targets AS MATERIALIZED (
|
|
1861
|
+
SELECT id, sequence FROM session_entries
|
|
1862
|
+
WHERE session_id = ? AND type = 'compaction' AND work_id = ?
|
|
1863
|
+
AND sequence < ? AND state != 'streaming'
|
|
1864
|
+
UNION ALL
|
|
1865
|
+
SELECT id, sequence
|
|
1866
|
+
FROM session_entries
|
|
1867
|
+
INDEXED BY idx_session_entries_cancellation_target
|
|
1868
|
+
WHERE session_id = ? AND type = 'cancellation'
|
|
1869
|
+
AND json_extract(data_json, '$.targetWorkId')
|
|
1870
|
+
IN (SELECT value FROM json_each(?))
|
|
1871
|
+
AND sequence < ? AND state != 'streaming'
|
|
1872
|
+
),
|
|
1873
|
+
${targetWalk(continues("child"))}
|
|
1874
|
+
${walkSteps(continues("walk"))}`, sessionId, workId, kept.sequence, sessionId, commands, kept.sequence, sessionId, kept.id, sessionId)
|
|
1875
|
+
.toArray());
|
|
1876
|
+
if (facts.length === 0)
|
|
1877
|
+
return Object.freeze([]);
|
|
1878
|
+
return Object.freeze(owner.sql
|
|
1879
|
+
.exec(`SELECT ${entryColumns}
|
|
1880
|
+
FROM session_entries
|
|
1881
|
+
WHERE id IN (SELECT value FROM json_each(?))
|
|
1882
|
+
ORDER BY sequence ASC`, JSON.stringify(facts))
|
|
1883
|
+
.toArray()
|
|
1884
|
+
.map((raw) => parseEntryRow(owner, raw)));
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* One lineage's settled window: the walk from `tipId` to the kept entry of
|
|
1888
|
+
* its newest complete compaction, or to the root. `compacted` says the walk
|
|
1889
|
+
* stopped at a kept entry rather than at the root. Only the walked rows are
|
|
1890
|
+
* read, and they are checked the way `locateLineage` checks a whole lineage.
|
|
1891
|
+
*/
|
|
1892
|
+
function readContextWindow(owner, sessionId, tipId) {
|
|
1893
|
+
const failures = lineageFailures.selected;
|
|
1894
|
+
const rows = owner.sql
|
|
1895
|
+
.exec(`${contextWalk}
|
|
1896
|
+
SELECT ${entryColumns}
|
|
1897
|
+
FROM session_entries
|
|
1898
|
+
WHERE id IN (SELECT id FROM walk) AND state != 'streaming'
|
|
1899
|
+
ORDER BY sequence ASC`, sessionId, tipId, sessionId)
|
|
1900
|
+
.toArray()
|
|
1901
|
+
.map((raw) => parseEntryRow(owner, raw));
|
|
1902
|
+
const kept = rows[0];
|
|
1903
|
+
if (kept === undefined)
|
|
1904
|
+
throw unavailable(failures.empty);
|
|
1905
|
+
let head;
|
|
1906
|
+
let previous;
|
|
1907
|
+
for (const row of rows) {
|
|
1908
|
+
if (previous !== undefined && row.parentId !== previous.id)
|
|
1909
|
+
throw unavailable(failures.invalid);
|
|
1910
|
+
if (row.entry.kind === "compaction")
|
|
1911
|
+
head = row.entry.head;
|
|
1912
|
+
previous = row;
|
|
1364
1913
|
}
|
|
1365
|
-
|
|
1914
|
+
const compacted = head === kept.id;
|
|
1915
|
+
if (!compacted && kept.parentId !== null)
|
|
1916
|
+
throw unavailable(failures.missingParent);
|
|
1917
|
+
return Object.freeze({ rows: Object.freeze(rows), compacted });
|
|
1366
1918
|
}
|
|
1367
1919
|
/**
|
|
1368
|
-
*
|
|
1920
|
+
* Context rows of the current selected lineage for one accepted work, or,
|
|
1921
|
+
* without a work, the settled context the next run starts from: the kept
|
|
1369
1922
|
* window through the tip, preceded only by the older facts that still decide
|
|
1370
1923
|
* it. Those are cancellations that withdrew a command inside the window, and
|
|
1371
1924
|
* earlier compactions of the active work, whose recovery runs at most once.
|
|
1925
|
+
* The read is bounded by the kept window and its deciding facts, not by the
|
|
1926
|
+
* history a compaction cut.
|
|
1372
1927
|
*/
|
|
1373
|
-
function
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
OR (type = 'cancellation'
|
|
1391
|
-
AND json_extract(data_json, '$.targetWorkId')
|
|
1392
|
-
IN (SELECT value FROM json_each(?))))
|
|
1393
|
-
ORDER BY sequence ASC`, start, workId, commands).map(parseEntryRow));
|
|
1394
|
-
return Object.freeze([...prefix, ...window]);
|
|
1928
|
+
export function readSessionContextPath(owner, input) {
|
|
1929
|
+
const leaf = readSessionRow(owner, input.sessionId)?.leaf_id;
|
|
1930
|
+
if (leaf === undefined || leaf === null)
|
|
1931
|
+
return Object.freeze([]);
|
|
1932
|
+
const window = readContextWindow(owner, input.sessionId, leaf);
|
|
1933
|
+
const kept = window.rows[0];
|
|
1934
|
+
if (!window.compacted || kept === undefined)
|
|
1935
|
+
return window.rows;
|
|
1936
|
+
return Object.freeze([
|
|
1937
|
+
...readDecidingFacts(owner, input.sessionId, kept, window.rows, input.workId ?? null),
|
|
1938
|
+
...window.rows,
|
|
1939
|
+
]);
|
|
1940
|
+
}
|
|
1941
|
+
/** Whether a row is an older fact that decides `commands` for `workId`. */
|
|
1942
|
+
function decides(row, workId, commands) {
|
|
1943
|
+
return ((row.entry.kind === "compaction" && row.entry.workId === workId) ||
|
|
1944
|
+
(row.entry.kind === "cancellation" && commands.has(row.entry.targetWorkId)));
|
|
1395
1945
|
}
|
|
1396
1946
|
/**
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1947
|
+
* The context a prepared run reads: its command's own lineage through the
|
|
1948
|
+
* command, then only this work's later rows on the current lineage (the
|
|
1949
|
+
* tail). The newest compaction of the tail, else of the command's lineage,
|
|
1950
|
+
* decides where the window starts, with the same deciding facts before it as
|
|
1951
|
+
* a settled context. The command's lineage is walked only from the command
|
|
1952
|
+
* down to that start, and the tail only down to the command.
|
|
1399
1953
|
*/
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1954
|
+
function readRunContextRows(owner, sessionId, command, current, workId) {
|
|
1955
|
+
const through = readSessionLineage(owner, { sessionId, leafId: command.id });
|
|
1956
|
+
if (through === undefined)
|
|
1957
|
+
throw unavailable("Accepted Session command lineage is unavailable");
|
|
1958
|
+
const tail = current === undefined
|
|
1959
|
+
? []
|
|
1960
|
+
: lineageQuery(owner, current, command.sequence, `SELECT ${entryColumns}
|
|
1961
|
+
FROM session_entries
|
|
1962
|
+
WHERE session_id = ? AND sequence > ? AND state != 'streaming'
|
|
1963
|
+
AND id IN (SELECT id FROM branch)
|
|
1964
|
+
AND (work_id = ?
|
|
1965
|
+
OR (type = 'command'
|
|
1966
|
+
AND json_extract(data_json, '$.targetWorkId') = ?))
|
|
1967
|
+
ORDER BY sequence ASC`, sessionId, command.sequence, workId, workId).map((raw) => parseEntryRow(owner, raw));
|
|
1968
|
+
let latest;
|
|
1969
|
+
for (const row of tail)
|
|
1970
|
+
if (row.entry.kind === "compaction")
|
|
1971
|
+
latest = row.entry.head;
|
|
1972
|
+
let start = 0;
|
|
1973
|
+
let lineageRows;
|
|
1974
|
+
if (latest === undefined) {
|
|
1975
|
+
const window = readContextWindow(owner, sessionId, command.id);
|
|
1976
|
+
lineageRows = window.rows;
|
|
1977
|
+
if (window.compacted)
|
|
1978
|
+
start = window.rows[0]?.sequence ?? 0;
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
const head = latest;
|
|
1982
|
+
start =
|
|
1983
|
+
tail.find(({ id }) => id === head)?.sequence ??
|
|
1984
|
+
lineageEntrySequence(owner, through, head) ??
|
|
1985
|
+
0;
|
|
1986
|
+
lineageRows =
|
|
1987
|
+
start > command.sequence
|
|
1988
|
+
? []
|
|
1989
|
+
: readLineageRange(owner, through, Math.max(start, 1), command.sequence);
|
|
1990
|
+
}
|
|
1991
|
+
if (start === 0)
|
|
1992
|
+
return Object.freeze([...lineageRows, ...tail]);
|
|
1993
|
+
const window = [
|
|
1994
|
+
...lineageRows,
|
|
1995
|
+
...tail.filter(({ sequence }) => sequence >= start),
|
|
1996
|
+
];
|
|
1997
|
+
const commands = new Set(window.flatMap(({ entry }) => entry.kind === "command" ? [entry.workId] : []));
|
|
1998
|
+
return Object.freeze([
|
|
1999
|
+
...readDecidingFacts(owner, sessionId, lineageRows[0] ?? command, window, workId),
|
|
2000
|
+
...tail.filter((row) => row.sequence < start && decides(row, workId, commands)),
|
|
2001
|
+
...window,
|
|
2002
|
+
]);
|
|
1405
2003
|
}
|
|
1406
2004
|
/** Work cancelled before delivery stays in history but never enters model context. */
|
|
1407
2005
|
export function withdrawnWorkIds(path) {
|
|
@@ -1431,13 +2029,12 @@ export function buildSessionContext(path, activeWorkId) {
|
|
|
1431
2029
|
const compactionEntry = compaction?.entry.kind === "compaction" ? compaction.entry : undefined;
|
|
1432
2030
|
const keptIndex = compactionEntry === undefined
|
|
1433
2031
|
? -1
|
|
1434
|
-
: path.findIndex(({ id }) => id === compactionEntry.
|
|
2032
|
+
: path.findIndex(({ id }) => id === compactionEntry.head);
|
|
1435
2033
|
const contextPath = keptIndex >= 0 ? path.slice(keptIndex) : path;
|
|
1436
2034
|
const visible = contextMessageIds(contextPath, activeWorkId);
|
|
1437
2035
|
const messages = [];
|
|
1438
|
-
if (compactionEntry !== undefined
|
|
1439
|
-
messages.push(
|
|
1440
|
-
}
|
|
2036
|
+
if (compactionEntry !== undefined)
|
|
2037
|
+
messages.push(...compactionEntry.model);
|
|
1441
2038
|
for (const row of contextPath) {
|
|
1442
2039
|
const prompt = promptCommand(row.entry);
|
|
1443
2040
|
if (prompt !== undefined && !withdrawn.has(prompt.workId))
|
|
@@ -1469,9 +2066,6 @@ export function prepareSessionRun(owner, input) {
|
|
|
1469
2066
|
encodeStoredSessionEntry(command.entry) !==
|
|
1470
2067
|
encodeStoredSessionEntry(admission.entry))
|
|
1471
2068
|
throw unavailable("Accepted Session command disagrees with its admission");
|
|
1472
|
-
const through = locateLineage(owner, input.sessionId, command.id, "selected");
|
|
1473
|
-
if (through.newestId !== command.id)
|
|
1474
|
-
throw unavailable("Accepted Session command lineage is unavailable");
|
|
1475
2069
|
// The current claim owns the command. Empty placeholders from a lost
|
|
1476
2070
|
// reader carry no recoverable text and must not survive as live progress.
|
|
1477
2071
|
owner.sql.exec("DELETE FROM session_entries WHERE session_id = ? AND work_id = ? AND state = 'streaming' AND type = 'message_start'", input.sessionId, command.entry.workId);
|
|
@@ -1479,19 +2073,14 @@ export function prepareSessionRun(owner, input) {
|
|
|
1479
2073
|
const workId = command.entry.workId;
|
|
1480
2074
|
const current = readSessionLineage(owner, { sessionId: input.sessionId });
|
|
1481
2075
|
// The command's own lineage, then only this work's later rows.
|
|
1482
|
-
const continuedPath =
|
|
1483
|
-
? [{ lineage: through, after: 0, work: null }]
|
|
1484
|
-
: [
|
|
1485
|
-
{ lineage: through, after: 0, work: null },
|
|
1486
|
-
{ lineage: current, after: command.sequence, work: workId },
|
|
1487
|
-
], workId);
|
|
2076
|
+
const continuedPath = readRunContextRows(owner, input.sessionId, command, current, workId);
|
|
1488
2077
|
const selectedCommand = Object.freeze({
|
|
1489
2078
|
...command,
|
|
1490
2079
|
entry: command.entry,
|
|
1491
2080
|
});
|
|
1492
2081
|
return Object.freeze({
|
|
1493
2082
|
command: selectedCommand,
|
|
1494
|
-
modelPreference: lineageModelPreference(owner,
|
|
2083
|
+
modelPreference: lineageModelPreference(owner, input.sessionId, command.id),
|
|
1495
2084
|
messages: buildSessionContext(continuedPath, command.entry.workId),
|
|
1496
2085
|
path: continuedPath,
|
|
1497
2086
|
entryDelivery,
|
|
@@ -1500,49 +2089,125 @@ export function prepareSessionRun(owner, input) {
|
|
|
1500
2089
|
});
|
|
1501
2090
|
});
|
|
1502
2091
|
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Whether a summary whose first kept entry is `head` still fits the selected
|
|
2094
|
+
* lineage (Pi Durable §6). It is stale when `head` is not on the selected
|
|
2095
|
+
* lineage or sits before the current context start, where a later compaction
|
|
2096
|
+
* already cut; placing it would bring cut history back. One bounded read.
|
|
2097
|
+
*/
|
|
2098
|
+
export function compactionHeadState(owner, input) {
|
|
2099
|
+
const leaf = readSessionRow(owner, input.sessionId)?.leaf_id;
|
|
2100
|
+
if (leaf === undefined || leaf === null)
|
|
2101
|
+
return "stale";
|
|
2102
|
+
// The head is current exactly when it lies in the settled window.
|
|
2103
|
+
const raw = owner.sql
|
|
2104
|
+
.exec(`${contextWalk}
|
|
2105
|
+
SELECT sequence
|
|
2106
|
+
FROM session_entries
|
|
2107
|
+
WHERE state != 'streaming' AND id = ? AND id IN (SELECT id FROM walk)`, input.sessionId, leaf, input.sessionId, input.head)
|
|
2108
|
+
.toArray()[0];
|
|
2109
|
+
if (raw === undefined)
|
|
2110
|
+
return "stale";
|
|
2111
|
+
const decoded = decodeLineageSequence(raw);
|
|
2112
|
+
if (decoded._tag === "Failure")
|
|
2113
|
+
throw unavailable("Stored Session entry sequence is invalid");
|
|
2114
|
+
return "current";
|
|
2115
|
+
}
|
|
1503
2116
|
/** Append one compaction notice on the selected lineage. */
|
|
1504
2117
|
export function appendCompactionNoticeOwned(owner, input) {
|
|
1505
2118
|
return owner.transactionSync(() => {
|
|
1506
|
-
const
|
|
2119
|
+
const common = {
|
|
1507
2120
|
kind: "compaction_notice",
|
|
1508
2121
|
workId: input.workId,
|
|
1509
2122
|
trigger: input.trigger,
|
|
1510
|
-
result: input.result,
|
|
1511
2123
|
};
|
|
2124
|
+
let entry;
|
|
2125
|
+
if (input.result === "stale" || input.result === "insufficient") {
|
|
2126
|
+
const dropped = { ...common, result: input.result };
|
|
2127
|
+
entry =
|
|
2128
|
+
input.usage === undefined
|
|
2129
|
+
? dropped
|
|
2130
|
+
: { ...dropped, usage: input.usage };
|
|
2131
|
+
}
|
|
2132
|
+
else if (input.result === "failed") {
|
|
2133
|
+
const failed = {
|
|
2134
|
+
...common,
|
|
2135
|
+
result: "failed",
|
|
2136
|
+
};
|
|
2137
|
+
const diagnosed = input.diagnostic === undefined
|
|
2138
|
+
? failed
|
|
2139
|
+
: { ...failed, diagnostic: input.diagnostic.slice(0, 200) };
|
|
2140
|
+
entry =
|
|
2141
|
+
input.usage === undefined
|
|
2142
|
+
? diagnosed
|
|
2143
|
+
: { ...diagnosed, usage: input.usage };
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
const nothing = {
|
|
2147
|
+
...common,
|
|
2148
|
+
result: "nothing_to_compact",
|
|
2149
|
+
};
|
|
2150
|
+
entry =
|
|
2151
|
+
input.diagnostic === undefined
|
|
2152
|
+
? nothing
|
|
2153
|
+
: { ...nothing, diagnostic: input.diagnostic.slice(0, 200) };
|
|
2154
|
+
}
|
|
1512
2155
|
return appendSessionEntry(owner, {
|
|
1513
2156
|
sessionId: input.sessionId,
|
|
1514
2157
|
id: sessionIds.fact.generate(),
|
|
1515
|
-
entry
|
|
1516
|
-
? entry
|
|
1517
|
-
: { ...entry, diagnostic: input.diagnostic.slice(0, 200) },
|
|
2158
|
+
entry,
|
|
1518
2159
|
});
|
|
1519
2160
|
});
|
|
1520
2161
|
}
|
|
1521
2162
|
/** Atomically append one compaction transition using the selected leaf CAS. */
|
|
1522
2163
|
export function appendCompactionTransitionOwned(owner, input) {
|
|
1523
|
-
return owner.transactionSync(() =>
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2164
|
+
return owner.transactionSync(() => appendCompactionRow(owner, input));
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Place a summary selected before now (a background compaction): the head
|
|
2168
|
+
* check of `compactionHeadState` and the leaf CAS in one transaction, so a
|
|
2169
|
+
* head that moved after any earlier check still makes it `stale` (Pi
|
|
2170
|
+
* Durable §6). A stale summary writes nothing.
|
|
2171
|
+
*/
|
|
2172
|
+
export function placeCompactionTransitionOwned(owner, input) {
|
|
2173
|
+
return owner.transactionSync(() => compactionHeadState(owner, {
|
|
2174
|
+
sessionId: input.sessionId,
|
|
2175
|
+
head: input.head,
|
|
2176
|
+
}) === "stale"
|
|
2177
|
+
? "stale"
|
|
2178
|
+
: appendCompactionRow(owner, input));
|
|
2179
|
+
}
|
|
2180
|
+
/** The leaf CAS and the compaction row; callers own the transaction. */
|
|
2181
|
+
function appendCompactionRow(owner, input) {
|
|
2182
|
+
const session = readSessionRow(owner, input.sessionId);
|
|
2183
|
+
if (session?.leaf_id !== input.expectedLeafId)
|
|
2184
|
+
throw new ShiitakeError("store", "conflict", "Session leaf changed during compaction");
|
|
2185
|
+
const entry = {
|
|
2186
|
+
kind: "compaction",
|
|
2187
|
+
workId: input.workId,
|
|
2188
|
+
head: input.head,
|
|
2189
|
+
model: [compactionSummaryMessage(input.summary, Date.now())],
|
|
2190
|
+
data: {
|
|
2191
|
+
reason: input.trigger,
|
|
1529
2192
|
resumesRun: input.resumesRun ?? false,
|
|
1530
|
-
trigger: input.trigger,
|
|
1531
|
-
tokensAfter: input.tokensAfter,
|
|
1532
|
-
workId: input.workId,
|
|
1533
|
-
firstKeptEntryId: input.firstKeptEntryId,
|
|
1534
|
-
summary: input.summary,
|
|
1535
2193
|
tokensBefore: input.tokensBefore,
|
|
2194
|
+
tokensAfter: input.tokensAfter,
|
|
1536
2195
|
usage: input.usage,
|
|
1537
2196
|
contextRevision: input.contextRevision,
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2197
|
+
},
|
|
2198
|
+
};
|
|
2199
|
+
const withReceipt = input.receipt === undefined
|
|
2200
|
+
? entry
|
|
2201
|
+
: { ...entry, data: { ...entry.data, receipt: input.receipt } };
|
|
2202
|
+
return appendSessionEntry(owner, {
|
|
2203
|
+
sessionId: input.sessionId,
|
|
2204
|
+
id: sessionIds.fact.generate(),
|
|
2205
|
+
entry: input.details === undefined
|
|
2206
|
+
? withReceipt
|
|
2207
|
+
: {
|
|
2208
|
+
...withReceipt,
|
|
2209
|
+
data: { ...withReceipt.data, details: input.details },
|
|
2210
|
+
},
|
|
1546
2211
|
});
|
|
1547
2212
|
}
|
|
1548
2213
|
function isPromptParts(content) {
|
|
@@ -1656,6 +2321,32 @@ export function acceptSessionCommandOwned(owner, sessionId, input, meta, origin,
|
|
|
1656
2321
|
});
|
|
1657
2322
|
return acceptanceFromAdmission(command, "new");
|
|
1658
2323
|
}
|
|
2324
|
+
/** Called only in the admission door's transaction, shared by admission and withdrawal. */
|
|
2325
|
+
export function assertSessionRequestNotWithdrawn(owner, sessionId, requestId) {
|
|
2326
|
+
if (owner.sql
|
|
2327
|
+
.exec("SELECT 1 FROM session_request_withdrawals WHERE session_id = ? AND request_id = ?", sessionId, requestId)
|
|
2328
|
+
.toArray().length > 0)
|
|
2329
|
+
throw new ShiitakeError("contract", "closed", "Request was withdrawn", requestId);
|
|
2330
|
+
}
|
|
2331
|
+
/** An admitted request wins; otherwise its key becomes a durable admission fence. */
|
|
2332
|
+
export function withdrawSessionRequestOwned(owner, sessionId, requestId) {
|
|
2333
|
+
const admitted = readSessionCommandAdmissionByRequest(owner, sessionId, requestId);
|
|
2334
|
+
if (admitted !== undefined)
|
|
2335
|
+
return { status: "accepted", commandId: admitted.id };
|
|
2336
|
+
// A held website reply records an operator line rather than scheduling work.
|
|
2337
|
+
// It is already durable and must not be restored as though it were unsent.
|
|
2338
|
+
const recorded = owner.sql
|
|
2339
|
+
.exec("SELECT id FROM session_entries WHERE session_id = ? AND request_id = ? AND type = 'operator'", sessionId, requestId)
|
|
2340
|
+
.toArray()[0];
|
|
2341
|
+
if (recorded !== undefined) {
|
|
2342
|
+
const decoded = decodeSessionIdRow(recorded);
|
|
2343
|
+
if (decoded._tag === "Failure")
|
|
2344
|
+
throw unavailable("Stored operator receipt is invalid");
|
|
2345
|
+
return { status: "accepted", commandId: decoded.success.id };
|
|
2346
|
+
}
|
|
2347
|
+
owner.sql.exec("INSERT OR IGNORE INTO session_request_withdrawals(session_id, request_id) VALUES (?, ?)", sessionId, requestId);
|
|
2348
|
+
return { status: "unknown" };
|
|
2349
|
+
}
|
|
1659
2350
|
function acceptanceFromAdmission(row, delivery) {
|
|
1660
2351
|
const accepted = Object.freeze({
|
|
1661
2352
|
sessionId: row.sessionId,
|
|
@@ -1811,6 +2502,7 @@ export function selectSessionBranch(owner, input) {
|
|
|
1811
2502
|
*/
|
|
1812
2503
|
export function appendOperatorEntry(owner, input) {
|
|
1813
2504
|
return owner.transactionSync(() => {
|
|
2505
|
+
assertSessionRequestNotWithdrawn(owner, input.sessionId, input.requestId);
|
|
1814
2506
|
if (readSessionRow(owner, input.sessionId) === undefined)
|
|
1815
2507
|
throw unavailable("Session is unavailable for an operator entry");
|
|
1816
2508
|
const existing = owner.sql
|
|
@@ -1846,13 +2538,12 @@ export function appendOperatorEntry(owner, input) {
|
|
|
1846
2538
|
}
|
|
1847
2539
|
function readSessionRows(owner, sessionId) {
|
|
1848
2540
|
return owner.sql
|
|
1849
|
-
.exec(`SELECT
|
|
1850
|
-
request_id, work_id, data_json, timestamp, completed_at
|
|
2541
|
+
.exec(`SELECT ${entryColumns}
|
|
1851
2542
|
FROM session_entries
|
|
1852
2543
|
WHERE session_id = ? AND state != 'streaming'
|
|
1853
2544
|
ORDER BY sequence ASC`, sessionId)
|
|
1854
2545
|
.toArray()
|
|
1855
|
-
.map(parseEntryRow);
|
|
2546
|
+
.map((raw) => parseEntryRow(owner, raw));
|
|
1856
2547
|
}
|
|
1857
2548
|
/** Link every stored row into a forest, retaining rows with no parent. */
|
|
1858
2549
|
function buildMutableSessionForest(rows) {
|
|
@@ -1952,7 +2643,7 @@ export function forkSessionAt(owner, input) {
|
|
|
1952
2643
|
const entry = row.entry.kind === "compaction"
|
|
1953
2644
|
? {
|
|
1954
2645
|
...row.entry,
|
|
1955
|
-
|
|
2646
|
+
head: idMap.get(row.entry.head) ??
|
|
1956
2647
|
(() => {
|
|
1957
2648
|
throw unavailable("Fork compaction boundary is unavailable");
|
|
1958
2649
|
})(),
|