@nanobpm/agentic 0.1.0 → 0.5.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/README.md +2 -1
- package/dist/demand/model.d.ts +7 -4
- package/dist/demand/model.js +22 -4
- package/dist/demand/taskdef.d.ts +13 -1
- package/dist/demand/taskdef.js +20 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol/conformance/frames.js +32 -4
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/payloads.d.ts +44 -0
- package/dist/protocol/payloads.js +61 -7
- package/dist/session/acp/client.d.ts +109 -0
- package/dist/session/acp/client.js +254 -0
- package/dist/session/acp/index.d.ts +27 -0
- package/dist/session/acp/index.js +27 -0
- package/dist/session/acp/jsonrpc.d.ts +25 -0
- package/dist/session/acp/jsonrpc.js +148 -0
- package/dist/session/acp/normalize.d.ts +48 -0
- package/dist/session/acp/normalize.js +162 -0
- package/dist/session/acp/protocol.d.ts +94 -0
- package/dist/session/acp/protocol.js +136 -0
- package/dist/session/acp/spawn.d.ts +36 -0
- package/dist/session/acp/spawn.js +68 -0
- package/dist/session/acp/transport.d.ts +62 -0
- package/dist/session/acp/transport.js +126 -0
- package/dist/session/adapter.d.ts +135 -0
- package/dist/session/adapter.js +24 -0
- package/dist/session/backend.d.ts +43 -0
- package/dist/session/backend.js +95 -0
- package/dist/session/events.d.ts +152 -0
- package/dist/session/events.js +192 -0
- package/dist/session/index.d.ts +31 -0
- package/dist/session/index.js +5 -0
- package/dist/session/log.d.ts +107 -0
- package/dist/session/log.js +351 -0
- package/dist/session/normalizer/claude.d.ts +23 -0
- package/dist/session/normalizer/claude.js +138 -0
- package/dist/session/normalizer/copilot.d.ts +27 -0
- package/dist/session/normalizer/copilot.js +105 -0
- package/dist/session/normalizer/deepseek.d.ts +11 -0
- package/dist/session/normalizer/deepseek.js +68 -0
- package/dist/session/normalizer/index.d.ts +36 -0
- package/dist/session/normalizer/index.js +29 -0
- package/dist/session/normalizer/kimi.d.ts +10 -0
- package/dist/session/normalizer/kimi.js +80 -0
- package/dist/session/normalizer/link.d.ts +36 -0
- package/dist/session/normalizer/link.js +56 -0
- package/dist/session/normalizer/pi.d.ts +13 -0
- package/dist/session/normalizer/pi.js +61 -0
- package/dist/session/normalizer/qwen.d.ts +11 -0
- package/dist/session/normalizer/qwen.js +65 -0
- package/dist/session/normalizer/record.d.ts +21 -0
- package/dist/session/normalizer/record.js +87 -0
- package/dist/session/normalizer/types.d.ts +139 -0
- package/dist/session/normalizer/types.js +31 -0
- package/dist/session/schema.d.ts +38 -0
- package/dist/session/schema.js +74 -0
- package/dist/transcript/index.d.ts +2 -2
- package/dist/transcript/index.js +1 -1
- package/dist/transcript/schema.d.ts +23 -1
- package/dist/transcript/schema.js +34 -1
- package/dist/transcript/store.d.ts +93 -5
- package/dist/transcript/store.js +287 -6
- package/package.json +17 -1
- package/src/demand/model.test.ts +82 -4
- package/src/demand/model.ts +30 -9
- package/src/demand/taskdef.test.ts +51 -6
- package/src/demand/taskdef.ts +31 -2
- package/src/index.ts +1 -0
- package/src/protocol/conformance/frames.ts +32 -4
- package/src/protocol/index.ts +4 -0
- package/src/protocol/payloads.test.ts +31 -1
- package/src/protocol/payloads.ts +110 -7
- package/src/session/acp/client.test.ts +222 -0
- package/src/session/acp/client.ts +356 -0
- package/src/session/acp/fake-agent.ts +71 -0
- package/src/session/acp/index.ts +68 -0
- package/src/session/acp/integration.test.ts +37 -0
- package/src/session/acp/jsonrpc.test.ts +75 -0
- package/src/session/acp/jsonrpc.ts +171 -0
- package/src/session/acp/normalize.test.ts +150 -0
- package/src/session/acp/normalize.ts +204 -0
- package/src/session/acp/protocol.ts +178 -0
- package/src/session/acp/spawn.test.ts +45 -0
- package/src/session/acp/spawn.ts +91 -0
- package/src/session/acp/transport.test.ts +82 -0
- package/src/session/acp/transport.ts +155 -0
- package/src/session/adapter.ts +159 -0
- package/src/session/backend.test.ts +198 -0
- package/src/session/backend.ts +128 -0
- package/src/session/events.test.ts +168 -0
- package/src/session/events.ts +347 -0
- package/src/session/index.ts +67 -0
- package/src/session/log.test.ts +215 -0
- package/src/session/log.ts +525 -0
- package/src/session/normalizer/backend-integration.test.ts +103 -0
- package/src/session/normalizer/claude.test.ts +68 -0
- package/src/session/normalizer/claude.ts +136 -0
- package/src/session/normalizer/copilot.test.ts +59 -0
- package/src/session/normalizer/copilot.ts +133 -0
- package/src/session/normalizer/deepseek.ts +80 -0
- package/src/session/normalizer/index.ts +61 -0
- package/src/session/normalizer/kimi.ts +82 -0
- package/src/session/normalizer/link.test.ts +24 -0
- package/src/session/normalizer/link.ts +81 -0
- package/src/session/normalizer/pi.ts +75 -0
- package/src/session/normalizer/probe.test.ts +49 -0
- package/src/session/normalizer/qwen.test.ts +20 -0
- package/src/session/normalizer/qwen.ts +77 -0
- package/src/session/normalizer/record.test.ts +68 -0
- package/src/session/normalizer/record.ts +88 -0
- package/src/session/normalizer/resume.test.ts +25 -0
- package/src/session/normalizer/types.ts +152 -0
- package/src/session/normalizer/vectors.test.ts +180 -0
- package/src/session/schema.test.ts +84 -0
- package/src/session/schema.ts +78 -0
- package/src/session/test-db.ts +56 -0
- package/src/transcript/index.ts +8 -0
- package/src/transcript/schema.test.ts +31 -4
- package/src/transcript/schema.ts +36 -1
- package/src/transcript/store.ts +438 -6
- package/src/transcript/turns.test.ts +334 -0
- package/dist/blackboard/test-db.d.ts +0 -5
- package/dist/blackboard/test-db.js +0 -42
- package/dist/presence/test-db.d.ts +0 -5
- package/dist/presence/test-db.js +0 -42
- package/dist/transcript/test-db.d.ts +0 -5
- package/dist/transcript/test-db.js +0 -41
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@nanobpm/agentic/session/normalizer` contract — ADR 0062, slice 3 (the
|
|
3
|
+
* `stream-json`/native-transcript **fallback** ingestion backend).
|
|
4
|
+
*
|
|
5
|
+
* Slice 2 speaks ACP directly; this slice covers every harness that does *not*
|
|
6
|
+
* (yet) speak ACP. ADR 0062 §5 frames `stream-json` as a *transport with N
|
|
7
|
+
* vendor dialects*, so there is no single "stream-json backend": each harness
|
|
8
|
+
* gets a small **normalizer** that maps its native/streaming session output onto
|
|
9
|
+
* Nano's canonical {@link SessionEvent} (slice 1), plus a **resume shim** over
|
|
10
|
+
* that harness's native `--resume <id>` (or SDK equivalent).
|
|
11
|
+
*
|
|
12
|
+
* The three moving parts a harness normalizer exposes:
|
|
13
|
+
*
|
|
14
|
+
* - {@link HarnessNormalizer.toDrafts} — the dialect map: one native record →
|
|
15
|
+
* zero-or-more {@link DraftEvent}s (canonical events *minus* the causal-chain
|
|
16
|
+
* fields the shared {@link linkDrafts} threads in, so a per-harness dialect
|
|
17
|
+
* never re-implements chaining).
|
|
18
|
+
* - {@link HarnessNormalizer.resume} — the resume shim: given a native session
|
|
19
|
+
* id, the exact native invocation ({@link ResumeShim}) that restores it.
|
|
20
|
+
* - {@link HarnessNormalizer.capabilities} — the {@link HarnessCapabilities}
|
|
21
|
+
* the {@link capabilityProbe} folds into a `durable-resume` advertisement
|
|
22
|
+
* (slice 5's enrolment gate reads this).
|
|
23
|
+
*
|
|
24
|
+
* Nothing here interprets a harness schema as *ours*: the native shapes are
|
|
25
|
+
* ingestion details owned entirely by each dialect module; the union they all
|
|
26
|
+
* target is the stable slice-1 contract.
|
|
27
|
+
*/
|
|
28
|
+
import type { SessionEvent } from "../events.ts";
|
|
29
|
+
/**
|
|
30
|
+
* Distributive `Omit` over a discriminated union: applies `Omit` to *each* union
|
|
31
|
+
* member, preserving the `type` discriminant. A plain `Omit<Union, K>` collapses
|
|
32
|
+
* to the members' common properties (losing the per-member fields), so we cannot
|
|
33
|
+
* use it to describe "a session event without its chain fields".
|
|
34
|
+
*/
|
|
35
|
+
export type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
36
|
+
/**
|
|
37
|
+
* A canonical {@link SessionEvent} as a dialect first produces it — the full
|
|
38
|
+
* semantic payload (`type` + type-specific fields) but *without* the causal-chain
|
|
39
|
+
* responsibilities (`parentId`, and an optional-only `id`). The shared
|
|
40
|
+
* {@link linkDrafts} threads `parentId` in emission order and fills any missing
|
|
41
|
+
* `id`, so an individual dialect never re-implements chain bookkeeping; it just
|
|
42
|
+
* says "here is the event this record means". A dialect that already knows a
|
|
43
|
+
* stable native id (a tool-call id, a provider message id) may supply it as
|
|
44
|
+
* `id` to preserve correlation across a resume.
|
|
45
|
+
*/
|
|
46
|
+
export type DraftEvent = DistributiveOmit<SessionEvent, "id" | "parentId"> & {
|
|
47
|
+
readonly id?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The native resume invocation a harness's shim resolves for a session id. Two
|
|
51
|
+
* transports cover the fleet:
|
|
52
|
+
*
|
|
53
|
+
* - `cli` — a flag-driven harness: `args` are the argv tail to append to the
|
|
54
|
+
* harness command to resume that session (e.g. Claude's `["--resume", id]`,
|
|
55
|
+
* Qwen's `["-r", id]`). Nano spawns; it never parses the harness's output
|
|
56
|
+
* beyond the dialect map.
|
|
57
|
+
* - `sdk` — an in-process harness (Copilot's `copilot-sdk`, the DeepSeek live
|
|
58
|
+
* feed): `call` names the SDK method and `args` are its arguments (e.g.
|
|
59
|
+
* `resumeSession(id)`), so the host invokes it directly rather than spawning.
|
|
60
|
+
*
|
|
61
|
+
* `sessionId` echoes the id the shim resumed, so a caller that only kept the
|
|
62
|
+
* {@link ResumeShim} still knows which session it targets.
|
|
63
|
+
*/
|
|
64
|
+
export type ResumeShim = {
|
|
65
|
+
readonly transport: "cli";
|
|
66
|
+
readonly sessionId: string;
|
|
67
|
+
readonly args: readonly string[];
|
|
68
|
+
} | {
|
|
69
|
+
readonly transport: "sdk";
|
|
70
|
+
readonly sessionId: string;
|
|
71
|
+
readonly call: string;
|
|
72
|
+
readonly args: readonly unknown[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* What a harness can do, from the perspective of durable resume. `streaming` is
|
|
76
|
+
* "exposes a machine-readable streaming/native mind source we can normalize";
|
|
77
|
+
* `resumeById` is "can restore a *specific* prior session by id" (not merely
|
|
78
|
+
* `--continue` the latest). {@link CapabilityAdvertisement.durableResume} is
|
|
79
|
+
* derived, never declared — see {@link capabilityProbe}.
|
|
80
|
+
*/
|
|
81
|
+
export interface HarnessCapabilities {
|
|
82
|
+
readonly streaming: boolean;
|
|
83
|
+
readonly resumeById: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The advertisement {@link capabilityProbe} produces: the raw capabilities plus
|
|
87
|
+
* the single **derived** `durableResume` bit slice 5's enrolment gate consumes.
|
|
88
|
+
* Keeping `durableResume` derived (never a hand-set field on a normalizer)
|
|
89
|
+
* eliminates the drift surface where a harness claims durability it can't honour.
|
|
90
|
+
*/
|
|
91
|
+
export interface CapabilityAdvertisement {
|
|
92
|
+
readonly harness: string;
|
|
93
|
+
readonly streaming: boolean;
|
|
94
|
+
readonly resumeById: boolean;
|
|
95
|
+
/** `true` iff the harness both streams a mind source AND resumes by id. */
|
|
96
|
+
readonly durableResume: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One harness's fallback ingestion adapter: a dialect map, a resume shim, and its
|
|
100
|
+
* capabilities. Independent per harness (they fan out in parallel), and all
|
|
101
|
+
* target the one canonical {@link SessionEvent} union.
|
|
102
|
+
*/
|
|
103
|
+
export interface HarnessNormalizer {
|
|
104
|
+
/** The harness id this normalizer speaks for, e.g. `"@github/copilot"`. */
|
|
105
|
+
readonly harness: string;
|
|
106
|
+
/** Raw capabilities; `durable-resume` is derived from these by {@link capabilityProbe}. */
|
|
107
|
+
readonly capabilities: HarnessCapabilities;
|
|
108
|
+
/**
|
|
109
|
+
* Map one native record (a parsed `stream-json` line, an SDK `SessionEvent`, a
|
|
110
|
+
* live-feed frame) to zero-or-more canonical {@link DraftEvent}s. Returns `[]`
|
|
111
|
+
* for records that carry no session-log meaning (transport keep-alives, init
|
|
112
|
+
* frames the canonical model does not represent). Throws
|
|
113
|
+
* {@link NormalizerDialectError} on a record that *should* map but is
|
|
114
|
+
* structurally invalid — a corrupt transcript fails loudly, it never
|
|
115
|
+
* fabricates an event.
|
|
116
|
+
*/
|
|
117
|
+
toDrafts(record: unknown): readonly DraftEvent[];
|
|
118
|
+
/** Resolve the native invocation that resumes `sessionId` for this harness. */
|
|
119
|
+
resume(sessionId: string): ResumeShim;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Derive a harness's `durable-resume` advertisement from its raw capabilities.
|
|
123
|
+
* A harness advertises `durable-resume` **iff** it both exposes a streaming mind
|
|
124
|
+
* source we can normalize AND can restore a specific session by id — either half
|
|
125
|
+
* alone is insufficient (a stream we can't resume, or a resume with no mind to
|
|
126
|
+
* replay). This is the single place the bit is computed.
|
|
127
|
+
*/
|
|
128
|
+
export declare function capabilityProbe(normalizer: HarnessNormalizer): CapabilityAdvertisement;
|
|
129
|
+
/**
|
|
130
|
+
* Raised when a native record that a dialect *should* map is structurally
|
|
131
|
+
* invalid (a missing tool-call id, a message with no content). Mirrors slice 1's
|
|
132
|
+
* `SessionEventShapeError` at the ingestion boundary: normalization is a trusted
|
|
133
|
+
* map, so a malformed native record surfaces loudly rather than silently
|
|
134
|
+
* dropping or fabricating a canonical event.
|
|
135
|
+
*/
|
|
136
|
+
export declare class NormalizerDialectError extends Error {
|
|
137
|
+
readonly harness: string;
|
|
138
|
+
constructor(harness: string, message: string);
|
|
139
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a harness's `durable-resume` advertisement from its raw capabilities.
|
|
3
|
+
* A harness advertises `durable-resume` **iff** it both exposes a streaming mind
|
|
4
|
+
* source we can normalize AND can restore a specific session by id — either half
|
|
5
|
+
* alone is insufficient (a stream we can't resume, or a resume with no mind to
|
|
6
|
+
* replay). This is the single place the bit is computed.
|
|
7
|
+
*/
|
|
8
|
+
export function capabilityProbe(normalizer) {
|
|
9
|
+
const { streaming, resumeById } = normalizer.capabilities;
|
|
10
|
+
return {
|
|
11
|
+
harness: normalizer.harness,
|
|
12
|
+
streaming,
|
|
13
|
+
resumeById,
|
|
14
|
+
durableResume: streaming && resumeById,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Raised when a native record that a dialect *should* map is structurally
|
|
19
|
+
* invalid (a missing tool-call id, a message with no content). Mirrors slice 1's
|
|
20
|
+
* `SessionEventShapeError` at the ingestion boundary: normalization is a trusted
|
|
21
|
+
* map, so a malformed native record surfaces loudly rather than silently
|
|
22
|
+
* dropping or fabricating a canonical event.
|
|
23
|
+
*/
|
|
24
|
+
export class NormalizerDialectError extends Error {
|
|
25
|
+
harness;
|
|
26
|
+
constructor(harness, message) {
|
|
27
|
+
super(`[${harness}] ${message}`);
|
|
28
|
+
this.name = "NormalizerDialectError";
|
|
29
|
+
this.harness = harness;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical authoritative session-log schema — ADR 0062, slice 1.
|
|
3
|
+
*
|
|
4
|
+
* The DDL here is the single source of truth {@link SqliteSessionLog.ensureSchema}
|
|
5
|
+
* applies, and is mirrored statement-for-statement by the app-boot migration
|
|
6
|
+
* `db/migrations/005_agentic_session.sql`. `schema.test.ts` normalises both and
|
|
7
|
+
* asserts they are identical — drift is a red test, not a silent boot mismatch.
|
|
8
|
+
* This mirrors the S6 transcript store's drift-guard exactly (ADR 0056 §12), the
|
|
9
|
+
* advisory precedent this authoritative log is promoted from.
|
|
10
|
+
*
|
|
11
|
+
* Three tables back the log:
|
|
12
|
+
* - `agentic_session_log` — one row per **activation** `(processInstanceKey,
|
|
13
|
+
* elementId)`: its fence high-water (`incarnation`), retention lifecycle,
|
|
14
|
+
* status, and the retained offset window (`first_offset` … `next_offset`).
|
|
15
|
+
* - `agentic_session_event` — the durable, authoritative events, keyed
|
|
16
|
+
* `(process_instance_key, element_id, event_offset)` so a re-lease can replay
|
|
17
|
+
* from any offset and a resuming incarnation can overwrite an uncommitted tail
|
|
18
|
+
* idempotently.
|
|
19
|
+
* - `agentic_session_checkpoint` — the mind/world join points, keyed
|
|
20
|
+
* `(process_instance_key, element_id, checkpoint_id)` with the pinned offset.
|
|
21
|
+
*
|
|
22
|
+
* `event_offset`/`checkpoint_offset` (not `offset`) is deliberate: `OFFSET` is a
|
|
23
|
+
* SQLite keyword, so the columns are named to avoid quoting it everywhere.
|
|
24
|
+
*/
|
|
25
|
+
/** The per-activation metadata + fence table name. */
|
|
26
|
+
export declare const SESSION_LOG_TABLE = "agentic_session_log";
|
|
27
|
+
/** The durable per-event table name. */
|
|
28
|
+
export declare const SESSION_EVENT_TABLE = "agentic_session_event";
|
|
29
|
+
/** The checkpoint table name. */
|
|
30
|
+
export declare const SESSION_CHECKPOINT_TABLE = "agentic_session_checkpoint";
|
|
31
|
+
/**
|
|
32
|
+
* The canonical session-log DDL. Forward-only and additive; every column added
|
|
33
|
+
* here must also be added to the boot migration (the drift guard enforces it).
|
|
34
|
+
* Events are immutable once committed under an incarnation; a resume overwrites
|
|
35
|
+
* only the *uncommitted* tail past the last checkpoint (a re-key at the same
|
|
36
|
+
* `(activation, offset)`), never a committed row.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SESSION_SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS agentic_session_log (\n process_instance_key TEXT NOT NULL,\n element_id TEXT NOT NULL,\n incarnation INTEGER NOT NULL DEFAULT 0,\n lifecycle TEXT NOT NULL DEFAULT 'activation',\n status TEXT NOT NULL DEFAULT 'open',\n created_at TEXT NOT NULL,\n completed_at TEXT,\n first_offset INTEGER,\n next_offset INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (process_instance_key, element_id)\n);\nCREATE TABLE IF NOT EXISTS agentic_session_event (\n process_instance_key TEXT NOT NULL,\n element_id TEXT NOT NULL,\n event_offset INTEGER NOT NULL,\n incarnation INTEGER NOT NULL,\n event_id TEXT NOT NULL,\n parent_id TEXT,\n event_type TEXT NOT NULL,\n payload TEXT NOT NULL,\n appended_at TEXT NOT NULL,\n PRIMARY KEY (process_instance_key, element_id, event_offset)\n);\nCREATE TABLE IF NOT EXISTS agentic_session_checkpoint (\n process_instance_key TEXT NOT NULL,\n element_id TEXT NOT NULL,\n checkpoint_id TEXT NOT NULL,\n checkpoint_offset INTEGER NOT NULL,\n incarnation INTEGER NOT NULL,\n commit_sha TEXT NOT NULL,\n effect_ledger TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (process_instance_key, element_id, checkpoint_id)\n);\nCREATE INDEX IF NOT EXISTS idx_agentic_session_checkpoint_offset ON agentic_session_checkpoint (process_instance_key, element_id, checkpoint_offset);\nCREATE INDEX IF NOT EXISTS idx_agentic_session_log_retention ON agentic_session_log (lifecycle, status, completed_at);";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical authoritative session-log schema — ADR 0062, slice 1.
|
|
3
|
+
*
|
|
4
|
+
* The DDL here is the single source of truth {@link SqliteSessionLog.ensureSchema}
|
|
5
|
+
* applies, and is mirrored statement-for-statement by the app-boot migration
|
|
6
|
+
* `db/migrations/005_agentic_session.sql`. `schema.test.ts` normalises both and
|
|
7
|
+
* asserts they are identical — drift is a red test, not a silent boot mismatch.
|
|
8
|
+
* This mirrors the S6 transcript store's drift-guard exactly (ADR 0056 §12), the
|
|
9
|
+
* advisory precedent this authoritative log is promoted from.
|
|
10
|
+
*
|
|
11
|
+
* Three tables back the log:
|
|
12
|
+
* - `agentic_session_log` — one row per **activation** `(processInstanceKey,
|
|
13
|
+
* elementId)`: its fence high-water (`incarnation`), retention lifecycle,
|
|
14
|
+
* status, and the retained offset window (`first_offset` … `next_offset`).
|
|
15
|
+
* - `agentic_session_event` — the durable, authoritative events, keyed
|
|
16
|
+
* `(process_instance_key, element_id, event_offset)` so a re-lease can replay
|
|
17
|
+
* from any offset and a resuming incarnation can overwrite an uncommitted tail
|
|
18
|
+
* idempotently.
|
|
19
|
+
* - `agentic_session_checkpoint` — the mind/world join points, keyed
|
|
20
|
+
* `(process_instance_key, element_id, checkpoint_id)` with the pinned offset.
|
|
21
|
+
*
|
|
22
|
+
* `event_offset`/`checkpoint_offset` (not `offset`) is deliberate: `OFFSET` is a
|
|
23
|
+
* SQLite keyword, so the columns are named to avoid quoting it everywhere.
|
|
24
|
+
*/
|
|
25
|
+
/** The per-activation metadata + fence table name. */
|
|
26
|
+
export const SESSION_LOG_TABLE = "agentic_session_log";
|
|
27
|
+
/** The durable per-event table name. */
|
|
28
|
+
export const SESSION_EVENT_TABLE = "agentic_session_event";
|
|
29
|
+
/** The checkpoint table name. */
|
|
30
|
+
export const SESSION_CHECKPOINT_TABLE = "agentic_session_checkpoint";
|
|
31
|
+
/**
|
|
32
|
+
* The canonical session-log DDL. Forward-only and additive; every column added
|
|
33
|
+
* here must also be added to the boot migration (the drift guard enforces it).
|
|
34
|
+
* Events are immutable once committed under an incarnation; a resume overwrites
|
|
35
|
+
* only the *uncommitted* tail past the last checkpoint (a re-key at the same
|
|
36
|
+
* `(activation, offset)`), never a committed row.
|
|
37
|
+
*/
|
|
38
|
+
export const SESSION_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS ${SESSION_LOG_TABLE} (
|
|
39
|
+
process_instance_key TEXT NOT NULL,
|
|
40
|
+
element_id TEXT NOT NULL,
|
|
41
|
+
incarnation INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
lifecycle TEXT NOT NULL DEFAULT 'activation',
|
|
43
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
44
|
+
created_at TEXT NOT NULL,
|
|
45
|
+
completed_at TEXT,
|
|
46
|
+
first_offset INTEGER,
|
|
47
|
+
next_offset INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
PRIMARY KEY (process_instance_key, element_id)
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS ${SESSION_EVENT_TABLE} (
|
|
51
|
+
process_instance_key TEXT NOT NULL,
|
|
52
|
+
element_id TEXT NOT NULL,
|
|
53
|
+
event_offset INTEGER NOT NULL,
|
|
54
|
+
incarnation INTEGER NOT NULL,
|
|
55
|
+
event_id TEXT NOT NULL,
|
|
56
|
+
parent_id TEXT,
|
|
57
|
+
event_type TEXT NOT NULL,
|
|
58
|
+
payload TEXT NOT NULL,
|
|
59
|
+
appended_at TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY (process_instance_key, element_id, event_offset)
|
|
61
|
+
);
|
|
62
|
+
CREATE TABLE IF NOT EXISTS ${SESSION_CHECKPOINT_TABLE} (
|
|
63
|
+
process_instance_key TEXT NOT NULL,
|
|
64
|
+
element_id TEXT NOT NULL,
|
|
65
|
+
checkpoint_id TEXT NOT NULL,
|
|
66
|
+
checkpoint_offset INTEGER NOT NULL,
|
|
67
|
+
incarnation INTEGER NOT NULL,
|
|
68
|
+
commit_sha TEXT NOT NULL,
|
|
69
|
+
effect_ledger TEXT NOT NULL,
|
|
70
|
+
created_at TEXT NOT NULL,
|
|
71
|
+
PRIMARY KEY (process_instance_key, element_id, checkpoint_id)
|
|
72
|
+
);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_${SESSION_CHECKPOINT_TABLE}_offset ON ${SESSION_CHECKPOINT_TABLE} (process_instance_key, element_id, checkpoint_offset);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_${SESSION_LOG_TABLE}_retention ON ${SESSION_LOG_TABLE} (lifecycle, status, completed_at);`;
|
|
@@ -14,5 +14,5 @@
|
|
|
14
14
|
* and kept in lockstep by a drift-guard test.
|
|
15
15
|
*/
|
|
16
16
|
export { TranscriptStore, TranscriptCorruptionError, TranscriptLifecycleError, systemClock } from "./store.ts";
|
|
17
|
-
export type { Clock, SqliteDb, TranscriptChunk, TranscriptLifecycle, TranscriptRing, TranscriptSlice, TranscriptStatus, TranscriptStoreOptions, TranscriptStream, } from "./store.ts";
|
|
18
|
-
export { TRANSCRIPT_CHUNK_TABLE, TRANSCRIPT_SCHEMA_SQL, TRANSCRIPT_STREAM_TABLE, } from "./schema.ts";
|
|
17
|
+
export type { Clock, SqliteDb, TranscriptChunk, TranscriptContentBlock, TranscriptContentType, TranscriptLifecycle, TranscriptRing, TranscriptSlice, TranscriptStatus, TranscriptStoreOptions, TranscriptStream, TranscriptToolCall, TranscriptTurn, TranscriptTurnMetrics, TranscriptTurnRole, } from "./store.ts";
|
|
18
|
+
export { TRANSCRIPT_CHUNK_TABLE, TRANSCRIPT_SCHEMA_SQL, TRANSCRIPT_STREAM_TABLE, TRANSCRIPT_TURN_SCHEMA_SQL, TRANSCRIPT_TURN_TABLE, } from "./schema.ts";
|
package/dist/transcript/index.js
CHANGED
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
* and kept in lockstep by a drift-guard test.
|
|
15
15
|
*/
|
|
16
16
|
export { TranscriptStore, TranscriptCorruptionError, TranscriptLifecycleError, systemClock } from "./store.js";
|
|
17
|
-
export { TRANSCRIPT_CHUNK_TABLE, TRANSCRIPT_SCHEMA_SQL, TRANSCRIPT_STREAM_TABLE, } from "./schema.js";
|
|
17
|
+
export { TRANSCRIPT_CHUNK_TABLE, TRANSCRIPT_SCHEMA_SQL, TRANSCRIPT_STREAM_TABLE, TRANSCRIPT_TURN_SCHEMA_SQL, TRANSCRIPT_TURN_TABLE, } from "./schema.js";
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* statement-for-statement identical — divergence is a red test, not a silent
|
|
10
10
|
* production/boot mismatch.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Three tables back the store:
|
|
13
13
|
* - `agentic_transcript_stream` — one row per relay stream: its retention
|
|
14
14
|
* lifecycle (`ephemeral` vs `long-lived`), its status (`open`/`completed`),
|
|
15
15
|
* and the offset window (`first_offset` … `next_offset`) currently retained.
|
|
16
16
|
* - `agentic_transcript_chunk` — the durable chunks, keyed `(stream, chunk_offset)`
|
|
17
17
|
* so a flush/append is idempotent and reattach can slice from any offset.
|
|
18
|
+
* - `agentic_transcript_turn` — the additive turn-structured view (Camunda
|
|
19
|
+
* `AgentHistoryRecordValue` parity, issue #475), keyed `(stream, turn_sequence)`.
|
|
20
|
+
* It is layered over — never a replacement for — the raw chunk stream.
|
|
18
21
|
*
|
|
19
22
|
* `chunk_offset` (not `offset`) is deliberate: `OFFSET` is a SQLite keyword, so
|
|
20
23
|
* the column is named to avoid quoting it in every statement.
|
|
@@ -23,6 +26,8 @@
|
|
|
23
26
|
export declare const TRANSCRIPT_STREAM_TABLE = "agentic_transcript_stream";
|
|
24
27
|
/** The durable per-chunk table name. */
|
|
25
28
|
export declare const TRANSCRIPT_CHUNK_TABLE = "agentic_transcript_chunk";
|
|
29
|
+
/** The durable per-turn (structured-view) table name. */
|
|
30
|
+
export declare const TRANSCRIPT_TURN_TABLE = "agentic_transcript_turn";
|
|
26
31
|
/**
|
|
27
32
|
* The canonical transcript-store DDL. Forward-only and additive; every column
|
|
28
33
|
* added here must also be added to the boot migration (the drift guard enforces
|
|
@@ -30,3 +35,20 @@ export declare const TRANSCRIPT_CHUNK_TABLE = "agentic_transcript_chunk";
|
|
|
30
35
|
* it never rewrites a chunk.
|
|
31
36
|
*/
|
|
32
37
|
export declare const TRANSCRIPT_SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS agentic_transcript_stream (\n stream TEXT PRIMARY KEY,\n lifecycle TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'open',\n created_at TEXT NOT NULL,\n completed_at TEXT,\n first_offset INTEGER,\n next_offset INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS agentic_transcript_chunk (\n stream TEXT NOT NULL,\n chunk_offset INTEGER NOT NULL,\n chunk TEXT NOT NULL,\n appended_at TEXT NOT NULL,\n PRIMARY KEY (stream, chunk_offset)\n);\nCREATE INDEX IF NOT EXISTS idx_agentic_transcript_stream_retention ON agentic_transcript_stream (lifecycle, status, completed_at);";
|
|
38
|
+
/**
|
|
39
|
+
* The turn-structured transcript DDL — the additive, Camunda-`AgentHistoryRecordValue`
|
|
40
|
+
* parity view layered over the raw chunk stream (issue #475). It ships as its own
|
|
41
|
+
* forward-only migration `db/migrations/008_agentic_transcript_turns.sql` (the raw
|
|
42
|
+
* chunk stream in {@link TRANSCRIPT_SCHEMA_SQL} is untouched — additive, no regression
|
|
43
|
+
* to existing readers), mirrored here as the single source of truth applied by
|
|
44
|
+
* {@link TranscriptStore.ensureSchema} and kept in lockstep by a drift-guard test.
|
|
45
|
+
*
|
|
46
|
+
* One row per structured turn, keyed `(stream, turn_sequence)` so an append/re-record
|
|
47
|
+
* is idempotent (exactly the `(stream, chunk_offset)` discipline of the chunk table).
|
|
48
|
+
* `turn_sequence` is the stream-local append order and idempotency key;
|
|
49
|
+
* `loop_iteration` is the agent-loop turn counter carried as data (Camunda allows
|
|
50
|
+
* several role-split records — e.g. ASSISTANT then TOOL_RESULT — within one iteration).
|
|
51
|
+
* `content`, `tool_calls` and `metrics` hold the typed content blocks, tool calls and
|
|
52
|
+
* per-turn metrics as JSON.
|
|
53
|
+
*/
|
|
54
|
+
export declare const TRANSCRIPT_TURN_SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS agentic_transcript_turn (\n stream TEXT NOT NULL,\n turn_sequence INTEGER NOT NULL,\n loop_iteration INTEGER NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n tool_calls TEXT NOT NULL,\n metrics TEXT,\n produced_at INTEGER,\n recorded_at TEXT NOT NULL,\n PRIMARY KEY (stream, turn_sequence)\n);";
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* statement-for-statement identical — divergence is a red test, not a silent
|
|
10
10
|
* production/boot mismatch.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Three tables back the store:
|
|
13
13
|
* - `agentic_transcript_stream` — one row per relay stream: its retention
|
|
14
14
|
* lifecycle (`ephemeral` vs `long-lived`), its status (`open`/`completed`),
|
|
15
15
|
* and the offset window (`first_offset` … `next_offset`) currently retained.
|
|
16
16
|
* - `agentic_transcript_chunk` — the durable chunks, keyed `(stream, chunk_offset)`
|
|
17
17
|
* so a flush/append is idempotent and reattach can slice from any offset.
|
|
18
|
+
* - `agentic_transcript_turn` — the additive turn-structured view (Camunda
|
|
19
|
+
* `AgentHistoryRecordValue` parity, issue #475), keyed `(stream, turn_sequence)`.
|
|
20
|
+
* It is layered over — never a replacement for — the raw chunk stream.
|
|
18
21
|
*
|
|
19
22
|
* `chunk_offset` (not `offset`) is deliberate: `OFFSET` is a SQLite keyword, so
|
|
20
23
|
* the column is named to avoid quoting it in every statement.
|
|
@@ -23,6 +26,8 @@
|
|
|
23
26
|
export const TRANSCRIPT_STREAM_TABLE = "agentic_transcript_stream";
|
|
24
27
|
/** The durable per-chunk table name. */
|
|
25
28
|
export const TRANSCRIPT_CHUNK_TABLE = "agentic_transcript_chunk";
|
|
29
|
+
/** The durable per-turn (structured-view) table name. */
|
|
30
|
+
export const TRANSCRIPT_TURN_TABLE = "agentic_transcript_turn";
|
|
26
31
|
/**
|
|
27
32
|
* The canonical transcript-store DDL. Forward-only and additive; every column
|
|
28
33
|
* added here must also be added to the boot migration (the drift guard enforces
|
|
@@ -46,3 +51,31 @@ CREATE TABLE IF NOT EXISTS ${TRANSCRIPT_CHUNK_TABLE} (
|
|
|
46
51
|
PRIMARY KEY (stream, chunk_offset)
|
|
47
52
|
);
|
|
48
53
|
CREATE INDEX IF NOT EXISTS idx_${TRANSCRIPT_STREAM_TABLE}_retention ON ${TRANSCRIPT_STREAM_TABLE} (lifecycle, status, completed_at);`;
|
|
54
|
+
/**
|
|
55
|
+
* The turn-structured transcript DDL — the additive, Camunda-`AgentHistoryRecordValue`
|
|
56
|
+
* parity view layered over the raw chunk stream (issue #475). It ships as its own
|
|
57
|
+
* forward-only migration `db/migrations/008_agentic_transcript_turns.sql` (the raw
|
|
58
|
+
* chunk stream in {@link TRANSCRIPT_SCHEMA_SQL} is untouched — additive, no regression
|
|
59
|
+
* to existing readers), mirrored here as the single source of truth applied by
|
|
60
|
+
* {@link TranscriptStore.ensureSchema} and kept in lockstep by a drift-guard test.
|
|
61
|
+
*
|
|
62
|
+
* One row per structured turn, keyed `(stream, turn_sequence)` so an append/re-record
|
|
63
|
+
* is idempotent (exactly the `(stream, chunk_offset)` discipline of the chunk table).
|
|
64
|
+
* `turn_sequence` is the stream-local append order and idempotency key;
|
|
65
|
+
* `loop_iteration` is the agent-loop turn counter carried as data (Camunda allows
|
|
66
|
+
* several role-split records — e.g. ASSISTANT then TOOL_RESULT — within one iteration).
|
|
67
|
+
* `content`, `tool_calls` and `metrics` hold the typed content blocks, tool calls and
|
|
68
|
+
* per-turn metrics as JSON.
|
|
69
|
+
*/
|
|
70
|
+
export const TRANSCRIPT_TURN_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS ${TRANSCRIPT_TURN_TABLE} (
|
|
71
|
+
stream TEXT NOT NULL,
|
|
72
|
+
turn_sequence INTEGER NOT NULL,
|
|
73
|
+
loop_iteration INTEGER NOT NULL,
|
|
74
|
+
role TEXT NOT NULL,
|
|
75
|
+
content TEXT NOT NULL,
|
|
76
|
+
tool_calls TEXT NOT NULL,
|
|
77
|
+
metrics TEXT,
|
|
78
|
+
produced_at INTEGER,
|
|
79
|
+
recorded_at TEXT NOT NULL,
|
|
80
|
+
PRIMARY KEY (stream, turn_sequence)
|
|
81
|
+
);`;
|
|
@@ -34,6 +34,73 @@ export interface TranscriptChunk {
|
|
|
34
34
|
readonly offset: number;
|
|
35
35
|
readonly chunk: string;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A structured turn's author role — the additive turn-structured view's parity
|
|
39
|
+
* with Camunda `AgentHistoryRole` (issue #475). One pass through the agent loop
|
|
40
|
+
* (model reasons → selects tools → evaluates results) is recorded as one or more
|
|
41
|
+
* role-tagged turns sharing a `loopIteration`.
|
|
42
|
+
*/
|
|
43
|
+
export type TranscriptTurnRole = "USER" | "ASSISTANT" | "TOOL_RESULT" | "CONFIGURATION" | "UNSPECIFIED";
|
|
44
|
+
/** Parity with Camunda `AgentHistoryContentType`: the type of a content block. */
|
|
45
|
+
export type TranscriptContentType = "TEXT" | "DOCUMENT" | "OBJECT" | "UNSPECIFIED";
|
|
46
|
+
/**
|
|
47
|
+
* A single typed content block in a turn's message, mirroring Camunda's
|
|
48
|
+
* `AgentHistoryMessageContentValue`. Exactly one payload is populated per the
|
|
49
|
+
* `contentType`: `text` for TEXT, `documentReference` for DOCUMENT, `object`
|
|
50
|
+
* (any JSON value) for OBJECT.
|
|
51
|
+
*/
|
|
52
|
+
export interface TranscriptContentBlock {
|
|
53
|
+
readonly contentType: TranscriptContentType;
|
|
54
|
+
/** Text payload; populated when `contentType` is TEXT. */
|
|
55
|
+
readonly text?: string;
|
|
56
|
+
/** Document reference; populated when `contentType` is DOCUMENT. */
|
|
57
|
+
readonly documentReference?: string;
|
|
58
|
+
/** JSON value payload; populated when `contentType` is OBJECT (any JSON type). */
|
|
59
|
+
readonly object?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A tool call embedded in a turn, mirroring Camunda's
|
|
63
|
+
* `AgentHistoryEmbeddedToolCallValue`: `toolCallId`, `toolName`, the tool task's
|
|
64
|
+
* `elementId`, and the `arguments` passed to it.
|
|
65
|
+
*/
|
|
66
|
+
export interface TranscriptToolCall {
|
|
67
|
+
readonly toolCallId: string;
|
|
68
|
+
readonly toolName: string;
|
|
69
|
+
readonly elementId?: string;
|
|
70
|
+
readonly arguments: Readonly<Record<string, unknown>>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Per-turn metrics, mirroring Camunda's `AgentHistoryMetricsValue`: the token
|
|
74
|
+
* counts consumed/produced by the turn's LLM call and its wall-clock duration.
|
|
75
|
+
*/
|
|
76
|
+
export interface TranscriptTurnMetrics {
|
|
77
|
+
readonly inputTokens: number;
|
|
78
|
+
readonly outputTokens: number;
|
|
79
|
+
readonly reasoningTokenCount: number;
|
|
80
|
+
readonly cacheCreationTokenCount: number;
|
|
81
|
+
readonly cacheReadTokenCount: number;
|
|
82
|
+
readonly durationMs: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A structured transcript turn — the additive Camunda `AgentHistoryRecordValue`
|
|
86
|
+
* parity view (issue #475). `sequence` is the stream-local append order and the
|
|
87
|
+
* idempotency key (mirroring a chunk's `offset`); `loopIteration` is the
|
|
88
|
+
* agent-loop turn counter carried as data (several role-split turns can share one
|
|
89
|
+
* iteration). Recording turns never touches the raw chunk stream.
|
|
90
|
+
*/
|
|
91
|
+
export interface TranscriptTurn {
|
|
92
|
+
/** Stream-local append order + idempotency key (like a chunk's `offset`). */
|
|
93
|
+
readonly sequence: number;
|
|
94
|
+
/** The agent-loop turn counter (Camunda `loopIteration`). */
|
|
95
|
+
readonly loopIteration: number;
|
|
96
|
+
readonly role: TranscriptTurnRole;
|
|
97
|
+
readonly content: readonly TranscriptContentBlock[];
|
|
98
|
+
readonly toolCalls: readonly TranscriptToolCall[];
|
|
99
|
+
/** Per-turn metrics; undefined when the worker reported none for this turn. */
|
|
100
|
+
readonly metrics?: TranscriptTurnMetrics;
|
|
101
|
+
/** Epoch-millis timestamp the turn was produced; undefined when unreported. */
|
|
102
|
+
readonly producedAt?: number;
|
|
103
|
+
}
|
|
37
104
|
/** Per-stream transcript metadata. */
|
|
38
105
|
export interface TranscriptStream {
|
|
39
106
|
readonly stream: string;
|
|
@@ -110,9 +177,11 @@ export declare class TranscriptStore {
|
|
|
110
177
|
get ephemeralRetentionMs(): number;
|
|
111
178
|
/**
|
|
112
179
|
* Apply the canonical transcript DDL (idempotent). Callers that let the app
|
|
113
|
-
* DataLayer migration runner apply
|
|
114
|
-
*
|
|
115
|
-
*
|
|
180
|
+
* DataLayer migration runner apply the transcript migrations
|
|
181
|
+
* (`db/migrations/002_agentic_transcript.sql` for the chunk stream and
|
|
182
|
+
* `db/migrations/008_agentic_transcript_turns.sql` for the turn-structured
|
|
183
|
+
* view) do not need this — but it is provided so the store is usable against a
|
|
184
|
+
* bare source too. The DDL is identical to the migrations (drift-guarded).
|
|
116
185
|
*/
|
|
117
186
|
ensureSchema(): void;
|
|
118
187
|
/**
|
|
@@ -163,6 +232,24 @@ export declare class TranscriptStore {
|
|
|
163
232
|
since(stream: string, from: number): TranscriptSlice;
|
|
164
233
|
/** Read a stream's whole durable transcript in offset order. */
|
|
165
234
|
read(stream: string): TranscriptChunk[];
|
|
235
|
+
/**
|
|
236
|
+
* Record structured turns into a stream's additive turn-structured view — the
|
|
237
|
+
* Camunda `AgentHistoryRecordValue` parity layer (issue #475). Each turn is
|
|
238
|
+
* keyed `(stream, sequence)` so re-recording an already-stored sequence (a
|
|
239
|
+
* retry, a re-emit, an overlapping reattach) is a no-op — never a duplicate,
|
|
240
|
+
* exactly the idempotency the chunk stream gets from `(stream, offset)`.
|
|
241
|
+
*
|
|
242
|
+
* This is purely additive: it never reads or writes the raw chunk stream or the
|
|
243
|
+
* stream's offset window, so it cannot regress any existing chunk reader. It
|
|
244
|
+
* auto-opens the stream (default `long-lived`) so the turns hang off a stream
|
|
245
|
+
* row; a lifecycle mismatch throws a {@link TranscriptLifecycleError} before
|
|
246
|
+
* writing anything (lifecycle is first-wins). The batch is atomic: an invalid
|
|
247
|
+
* turn (or any failed write) partway through rolls the whole call back — it
|
|
248
|
+
* records every turn or none. Returns the number of newly-persisted turns.
|
|
249
|
+
*/
|
|
250
|
+
recordTurns(stream: string, turns: Iterable<TranscriptTurn>, lifecycle?: TranscriptLifecycle): number;
|
|
251
|
+
/** Read a stream's whole turn-structured transcript in `sequence` order. */
|
|
252
|
+
readTurns(stream: string): TranscriptTurn[];
|
|
166
253
|
/**
|
|
167
254
|
* Apply a rolling retention window to a long-lived stream: drop every chunk with
|
|
168
255
|
* `offset < before`. A subsequent {@link since} from an offset older than
|
|
@@ -174,8 +261,9 @@ export declare class TranscriptStore {
|
|
|
174
261
|
/**
|
|
175
262
|
* Retention sweep for completed ephemeral transcripts: drop every stream whose
|
|
176
263
|
* `status = 'completed'` and whose `completed_at` is older than the retention
|
|
177
|
-
* window, along with its chunks. Long-lived streams are
|
|
178
|
-
* are bounded by {@link truncateBefore} instead). Returns
|
|
264
|
+
* window, along with its chunks and structured turns. Long-lived streams are
|
|
265
|
+
* never time-swept (they are bounded by {@link truncateBefore} instead). Returns
|
|
266
|
+
* the removed stream ids.
|
|
179
267
|
*
|
|
180
268
|
* The selection and all deletes run inside a single SAVEPOINT (#atomic) so the
|
|
181
269
|
* sweep is all-or-nothing: if any delete throws mid-sweep the whole batch rolls
|