@code-yeongyu/senpi-agent-core 2026.9.30 → 2026.10.1-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +49 -20
  2. package/dist/agent-loop.d.ts +27 -3
  3. package/dist/agent-loop.js +192 -56
  4. package/dist/agent.d.ts +21 -7
  5. package/dist/agent.js +38 -18
  6. package/dist/harness/execution/tools.d.ts +1 -1
  7. package/dist/harness/execution/tools.js +0 -2
  8. package/dist/harness/messages.js +1 -0
  9. package/dist/harness/pico3/bash.d.ts +10 -0
  10. package/dist/harness/pico3/bash.js +25 -0
  11. package/dist/harness/pico3/bounded.d.ts +21 -0
  12. package/dist/harness/pico3/bounded.js +95 -0
  13. package/dist/harness/pico3/chord.d.ts +41 -0
  14. package/dist/harness/pico3/chord.js +147 -0
  15. package/dist/harness/pico3/context.d.ts +16 -0
  16. package/dist/harness/pico3/context.js +88 -0
  17. package/dist/harness/pico3/harness.d.ts +198 -0
  18. package/dist/harness/pico3/harness.js +645 -0
  19. package/dist/harness/pico3/hooks.d.ts +10 -0
  20. package/dist/harness/pico3/hooks.js +33 -0
  21. package/dist/harness/pico3/index.d.ts +19 -0
  22. package/dist/harness/pico3/index.js +15 -0
  23. package/dist/harness/pico3/jsonl.d.ts +58 -0
  24. package/dist/harness/pico3/jsonl.js +322 -0
  25. package/dist/harness/pico3/kinds/collapse.d.ts +49 -0
  26. package/dist/harness/pico3/kinds/collapse.js +191 -0
  27. package/dist/harness/pico3/kinds/entries.d.ts +78 -0
  28. package/dist/harness/pico3/kinds/entries.js +13 -0
  29. package/dist/harness/pico3/kinds/frames.d.ts +9 -0
  30. package/dist/harness/pico3/kinds/frames.js +75 -0
  31. package/dist/harness/pico3/kinds/generation.d.ts +94 -0
  32. package/dist/harness/pico3/kinds/generation.js +504 -0
  33. package/dist/harness/pico3/kinds/job.d.ts +44 -0
  34. package/dist/harness/pico3/kinds/job.js +143 -0
  35. package/dist/harness/pico3/kinds/plugin.d.ts +15 -0
  36. package/dist/harness/pico3/kinds/plugin.js +35 -0
  37. package/dist/harness/pico3/kinds/post-tools.d.ts +22 -0
  38. package/dist/harness/pico3/kinds/post-tools.js +145 -0
  39. package/dist/harness/pico3/kinds/task-api.d.ts +3 -0
  40. package/dist/harness/pico3/kinds/task-api.js +45 -0
  41. package/dist/harness/pico3/kinds/tool.d.ts +43 -0
  42. package/dist/harness/pico3/kinds/tool.js +373 -0
  43. package/dist/harness/pico3/legacy-tracker.d.ts +11 -0
  44. package/dist/harness/pico3/legacy-tracker.js +39 -0
  45. package/dist/harness/pico3/membrane.d.ts +23 -0
  46. package/dist/harness/pico3/membrane.js +140 -0
  47. package/dist/harness/pico3/memory.d.ts +53 -0
  48. package/dist/harness/pico3/memory.js +265 -0
  49. package/dist/harness/pico3/scheduler.d.ts +49 -0
  50. package/dist/harness/pico3/scheduler.js +437 -0
  51. package/dist/harness/pico3/session.d.ts +264 -0
  52. package/dist/harness/pico3/session.js +1328 -0
  53. package/dist/harness/pico3/system.d.ts +126 -0
  54. package/dist/harness/pico3/system.js +244 -0
  55. package/dist/harness/pico3/types.d.ts +973 -0
  56. package/dist/harness/pico3/types.js +94 -0
  57. package/dist/harness/pico3/view.d.ts +34 -0
  58. package/dist/harness/pico3/view.js +404 -0
  59. package/dist/harness/runtime/drive/tool-placement.js +2 -27
  60. package/dist/harness/telemetry.d.ts +36 -36
  61. package/dist/harness/tools/image.js +1 -1
  62. package/dist/proxy.d.ts +2 -2
  63. package/dist/types.d.ts +111 -34
  64. package/package.json +9 -5
@@ -0,0 +1,373 @@
1
+ import { Errors } from "typebox/value";
2
+ import { Bounded } from "../bounded.js";
3
+ import { toStored, } from "../types.js";
4
+ import { taskApi } from "./task-api.js";
5
+ const synthetic = (text, code) => ({
6
+ content: [{ type: "text", text }],
7
+ isError: true,
8
+ diagnostics: [{ severity: "error", message: text, code }],
9
+ });
10
+ /** Validate arguments with the tool's real schema (TypeBox 1.x). */
11
+ export function invalid(declaration, call) {
12
+ const errors = [...Errors(declaration.parameters, call.arguments)];
13
+ return errors.length === 0 ? undefined : errors.map((e) => `${e.instancePath || "/"}: ${e.message}`).join("; ");
14
+ }
15
+ const sameIdentity = (a, b) => a.id === b.id && a.name === b.name && a.namespace === b.namespace;
16
+ export const tool = {
17
+ name: "pi.tool",
18
+ turn: true,
19
+ inflight: ["started"],
20
+ // Offered-set check, lookup, validate, beforeTool, validate again; then write `started`
21
+ // (the in-flight checkpoint) and invoke. A crash before `started` reruns all of this.
22
+ async initial(task, rt, ctx) {
23
+ const call = task.input.call;
24
+ if (!task.input.offered.includes(call.name))
25
+ return {
26
+ done: close(task, synthetic(`tool ${call.name} was not offered`, "not_offered"), undefined, rt.now()),
27
+ };
28
+ const declaration = rt.tools.get(call.name);
29
+ if (declaration === undefined)
30
+ return {
31
+ done: close(task, synthetic(`tool ${call.name} is not registered`, "missing_tool"), undefined, rt.now()),
32
+ };
33
+ const bad = invalid(declaration, call);
34
+ if (bad !== undefined)
35
+ return {
36
+ done: close(task, synthetic(`invalid arguments: ${bad}`, "invalid_arguments"), declaration, rt.now()),
37
+ };
38
+ const hooked = await beforeTool(task, rt, call, ctx);
39
+ if ("block" in hooked)
40
+ return { done: close(task, synthetic(`blocked: ${hooked.block}`, "blocked"), declaration, rt.now()) };
41
+ if (!sameIdentity(call, hooked.call))
42
+ return { done: close(task, synthetic("blocked: call identity changed", "blocked"), declaration, rt.now()) };
43
+ const badFinal = invalid(declaration, hooked.call);
44
+ if (badFinal !== undefined)
45
+ return {
46
+ done: close(task, synthetic(`invalid arguments after hook: ${badFinal}`, "invalid_arguments"), declaration, rt.now()),
47
+ };
48
+ const final = toStored(hooked.call);
49
+ await rt.commit((tx) => {
50
+ tx.checkpoint({ phase: "started", replay: declaration.replay ?? "unsafe", call: final });
51
+ const slot = tx.toolSlot(task);
52
+ slot.status = "running";
53
+ delete slot.waitingOn;
54
+ tx.emit({ type: "tool.started", taskId: task.id, callId: final.id, name: final.name });
55
+ }, ctx); // durable before the effect
56
+ return { done: await invoke(task, final, declaration, rt, ctx) };
57
+ },
58
+ phases: {
59
+ // Only entered by the scheduler after reopen. The stored final call is the evidence; beforeTool never reruns.
60
+ async started(task, rt, ctx) {
61
+ const cp = task.checkpoint;
62
+ const declaration = rt.tools.get(cp.call.name);
63
+ if (declaration === undefined)
64
+ return {
65
+ done: close(task, synthetic(`tool ${cp.call.name} unavailable after restart`, "unavailable"), undefined, rt.now(), cp.call),
66
+ };
67
+ if (cp.replay !== "safe" || (declaration.replay ?? "unsafe") !== "safe")
68
+ return {
69
+ done: close(task, synthetic(`tool ${cp.call.name} was interrupted`, "interrupted"), declaration, rt.now(), cp.call),
70
+ };
71
+ if (invalid(declaration, cp.call) !== undefined)
72
+ return {
73
+ done: close(task, synthetic(`tool ${cp.call.name} was interrupted; arguments no longer validate`, "interrupted"), declaration, rt.now(), cp.call),
74
+ };
75
+ return { done: await invoke(task, cp.call, declaration, rt, ctx) };
76
+ },
77
+ },
78
+ async abort(task, rt, ctx) {
79
+ for (const conversationId of task.owns) {
80
+ const children = await rt.commit((tx) => tx.tasks({ conversationId, status: ["pending", "running"] }), ctx);
81
+ for (const child of children)
82
+ if (!child.background)
83
+ await rt.abortTask(child.id, ctx);
84
+ }
85
+ return (tx, current) => {
86
+ const call = task.checkpoint?.call ?? task.input.call;
87
+ const entry = tx.appendEntry(current.conversationId, {
88
+ kind: "pi.tool_result",
89
+ model: [toMessage(call, synthetic("tool aborted", "aborted"), rt.now())],
90
+ data: { diagnostics: [{ severity: "error", message: "aborted", code: "aborted" }] },
91
+ });
92
+ const slot = tx.sticky(current.conversationId).turn.tools[task.input.index];
93
+ if (slot !== undefined) {
94
+ slot.status = "aborted";
95
+ slot.entry = entry;
96
+ delete slot.waitingOn;
97
+ }
98
+ tx.emit({ type: "tool.aborted", taskId: current.id, callId: call.id, entry });
99
+ return { entry };
100
+ };
101
+ },
102
+ };
103
+ /** The one point where a throwing handler blocks rather than being skipped. */
104
+ async function beforeTool(task, rt, call, ctx) {
105
+ let current = call;
106
+ for (const binding of rt.hooks.handlers()) {
107
+ function memo(name, candidateOrContext, context) {
108
+ const targetContext = context ?? candidateOrContext;
109
+ return rt.commit((tx) => {
110
+ tx.plugins(binding.namespace);
111
+ const slot = tx.toolSlot(task);
112
+ if (!Object.hasOwn(slot, "memos"))
113
+ slot.memos = {};
114
+ const memos = slot.memos;
115
+ const key = `hook:${binding.namespace.id}:${name}`;
116
+ if (Object.hasOwn(memos, key))
117
+ return structuredClone(memos[key]);
118
+ if (context === undefined)
119
+ return undefined;
120
+ const candidate = structuredClone(candidateOrContext);
121
+ memos[key] = candidate;
122
+ return candidate;
123
+ }, targetContext);
124
+ }
125
+ const api = {
126
+ ...binding.api,
127
+ callId: call.id,
128
+ waiting: (context) => rt.commit((tx, currentTask) => {
129
+ tx.plugins(binding.namespace);
130
+ const slot = tx.toolSlot(currentTask);
131
+ if (slot.waitingOn === binding.namespace.id)
132
+ return;
133
+ slot.waitingOn = binding.namespace.id;
134
+ tx.emit({
135
+ type: "tool.waiting",
136
+ taskId: currentTask.id,
137
+ callId: call.id,
138
+ on: binding.namespace.id,
139
+ });
140
+ }, context),
141
+ memo,
142
+ emit: (name, data, context) => rt.commit((tx) => tx.emit(binding.namespace, name, data), context),
143
+ };
144
+ try {
145
+ const value = await binding.handlers.beforeTool?.(current, api, ctx);
146
+ if (!value)
147
+ continue;
148
+ if (value.block !== undefined)
149
+ return { block: value.block };
150
+ if (value.call !== undefined)
151
+ current = value.call;
152
+ }
153
+ catch (error) {
154
+ if (ctx.abortSignal?.aborted)
155
+ throw error;
156
+ return { block: `hook threw: ${String(error)}` };
157
+ }
158
+ }
159
+ return { call: current };
160
+ }
161
+ export const DEFAULT_BOUNDS = { maxBytes: 64 * 1024, maxLines: 200, retain: "head" };
162
+ async function invoke(task, call, declaration, rt, ctx) {
163
+ // The kernel owns the stream: one bounded buffer, one throttle, one flush path. Flush failures are not swallowed.
164
+ const b = { ...DEFAULT_BOUNDS, ...declaration.output };
165
+ const buffer = new Bounded(b.maxBytes, b.maxLines, b.retain);
166
+ let streamed = false;
167
+ let lastFlush = 0;
168
+ let flushing = Promise.resolve();
169
+ let flushError;
170
+ const flush = () => {
171
+ lastFlush = rt.now();
172
+ const text = buffer.text();
173
+ flushing = flushing
174
+ .then(() => rt.commit((tx) => {
175
+ tx.toolSlot(task).output = text;
176
+ }, ctx))
177
+ .catch((e) => {
178
+ flushError ??= e;
179
+ });
180
+ };
181
+ const api = {
182
+ ...taskApi(task, rt),
183
+ // Only the free fields; identity (callId/name/args/output) is protected.
184
+ progress: (update, c) => rt.commit((tx) => {
185
+ const slot = tx.toolSlot(task);
186
+ const free = {
187
+ progress: slot.progress,
188
+ details: slot.details,
189
+ continuedBy: slot.continuedBy,
190
+ };
191
+ update(free);
192
+ if (free.progress !== undefined)
193
+ slot.progress = free.progress;
194
+ else
195
+ delete slot.progress;
196
+ if (free.details !== undefined)
197
+ slot.details = toStored(free.details);
198
+ else
199
+ delete slot.details;
200
+ if (free.continuedBy !== undefined)
201
+ slot.continuedBy = free.continuedBy;
202
+ else
203
+ delete slot.continuedBy;
204
+ }, c),
205
+ stream(chunk) {
206
+ streamed = true;
207
+ buffer.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
208
+ if (rt.now() - lastFlush >= 100)
209
+ flush();
210
+ },
211
+ };
212
+ function memo(name, candidateOrContext, context) {
213
+ const targetContext = context ?? candidateOrContext;
214
+ return rt.commit((tx) => {
215
+ const slot = tx.toolSlot(task);
216
+ if (!Object.hasOwn(slot, "memos"))
217
+ slot.memos = {};
218
+ const memos = slot.memos;
219
+ const key = `tool:${name}`;
220
+ if (Object.hasOwn(memos, key))
221
+ return structuredClone(memos[key]);
222
+ if (context === undefined)
223
+ return undefined;
224
+ const candidate = structuredClone(candidateOrContext);
225
+ memos[key] = candidate;
226
+ return candidate;
227
+ }, targetContext);
228
+ }
229
+ const toolApi = { ...api, callId: call.id, memo };
230
+ let result;
231
+ try {
232
+ result = await declaration.execute(call.arguments, toolApi, ctx);
233
+ }
234
+ catch (error) {
235
+ if (ctx.abortSignal?.aborted)
236
+ throw error;
237
+ result = synthetic(`tool threw: ${String(error)}`, "threw");
238
+ }
239
+ if (streamed) {
240
+ flush();
241
+ await flushing;
242
+ if (flushError !== undefined)
243
+ throw flushError; // a persistence failure is never hidden
244
+ if (result.content === undefined)
245
+ result = {
246
+ ...result,
247
+ content: [{ type: "text", text: buffer.text() }],
248
+ ...(buffer.dropped
249
+ ? {
250
+ diagnostics: [
251
+ ...(result.diagnostics ?? []),
252
+ {
253
+ severity: "warn",
254
+ code: "truncated",
255
+ message: `${buffer.droppedBytes} bytes / ${buffer.droppedLines} lines dropped (${b.retain} ${b.maxBytes} retained)`,
256
+ },
257
+ ],
258
+ }
259
+ : {}),
260
+ };
261
+ }
262
+ let final = result;
263
+ await rt.hooks.each(ctx, (h, api) => h.afterTool?.(call, final, { ...api, callId: call.id }, ctx), (value) => {
264
+ if (value !== undefined)
265
+ final = value;
266
+ });
267
+ return close(task, final, declaration, rt.now(), call, streamed ? { bytes: buffer.droppedBytes, lines: buffer.droppedLines } : undefined);
268
+ }
269
+ /** Closure: bound the output, store the exact model message under the stored final call, rest as strict-JSON data. */
270
+ function close(task, raw, declaration, now, call = task.input.call, streamTruncated) {
271
+ return (tx, current) => {
272
+ const bounded = bound({ ...raw, content: raw.content ?? [] }, declaration?.output);
273
+ const result = bounded.result;
274
+ const bytes = (bounded.truncated?.bytes ?? 0) + (streamTruncated?.bytes ?? 0);
275
+ const lines = (bounded.truncated?.lines ?? 0) + (streamTruncated?.lines ?? 0);
276
+ const truncated = bytes > 0 || lines > 0 ? { bytes, lines } : undefined;
277
+ const data = {};
278
+ if (result.details !== undefined)
279
+ data.details = toStored(result.details);
280
+ if (result.diagnostics !== undefined)
281
+ data.diagnostics = toStored(result.diagnostics);
282
+ if (result.control !== undefined)
283
+ data.control = toStored(result.control);
284
+ if (truncated !== undefined)
285
+ data.truncated = truncated;
286
+ const entry = tx.appendEntry(current.conversationId, {
287
+ kind: "pi.tool_result",
288
+ model: [toMessage(call, result, now)],
289
+ data,
290
+ });
291
+ const slot = tx.toolSlot(task);
292
+ slot.status = result.isError === true ? "error" : "done";
293
+ slot.entry = entry;
294
+ delete slot.waitingOn;
295
+ tx.emit({
296
+ type: "tool.finished",
297
+ taskId: current.id,
298
+ callId: call.id,
299
+ entry,
300
+ isError: result.isError === true,
301
+ ...(result.control === undefined ? {} : { control: result.control }),
302
+ });
303
+ if (truncated !== undefined)
304
+ tx.emit({
305
+ type: "warning",
306
+ source: "tool",
307
+ message: `tool output truncated: ${truncated.bytes} bytes / ${truncated.lines} lines dropped`,
308
+ });
309
+ const completion = {
310
+ status: "completed",
311
+ result: { entry, ...(result.control === undefined ? {} : { control: toStored(result.control) }) },
312
+ };
313
+ return completion;
314
+ };
315
+ }
316
+ function toMessage(call, result, now) {
317
+ return toStored({
318
+ role: "toolResult",
319
+ toolCallId: call.id,
320
+ toolName: call.name,
321
+ content: result.content ?? [],
322
+ isError: result.isError ?? false,
323
+ timestamp: now,
324
+ });
325
+ }
326
+ function bound(result, declared) {
327
+ const bounds = { ...DEFAULT_BOUNDS, ...declared };
328
+ const blocks = result.content ?? [];
329
+ let text = blocks
330
+ .filter((block) => block.type === "text")
331
+ .map((block) => block.text)
332
+ .join("");
333
+ let droppedLines = 0;
334
+ let droppedBytes = 0;
335
+ const lines = text.split("\n");
336
+ if (lines.length > bounds.maxLines) {
337
+ droppedLines = lines.length - bounds.maxLines;
338
+ text = (bounds.retain === "head" ? lines.slice(0, bounds.maxLines) : lines.slice(-bounds.maxLines)).join("\n");
339
+ }
340
+ const bytes = new TextEncoder().encode(text);
341
+ if (bytes.length > bounds.maxBytes) {
342
+ droppedBytes = bytes.length - bounds.maxBytes;
343
+ text = new TextDecoder().decode(bounds.retain === "head" ? bytes.subarray(0, bounds.maxBytes) : bytes.subarray(-bounds.maxBytes));
344
+ }
345
+ if (droppedBytes === 0 && droppedLines === 0)
346
+ return { result };
347
+ let textIndex = -1;
348
+ for (let index = 0; index < blocks.length; index++) {
349
+ if (blocks[index].type !== "text")
350
+ continue;
351
+ if (bounds.retain === "head" && textIndex >= 0)
352
+ continue;
353
+ textIndex = index;
354
+ }
355
+ const content = [];
356
+ for (let index = 0; index < blocks.length; index++) {
357
+ const block = blocks[index];
358
+ if (block.type !== "text")
359
+ content.push(block);
360
+ else if (index === textIndex)
361
+ content.push({ ...block, text });
362
+ }
363
+ const note = {
364
+ severity: "warn",
365
+ code: "truncated",
366
+ message: `output truncated: ${droppedLines} lines, ${droppedBytes} bytes dropped`,
367
+ };
368
+ return {
369
+ result: { ...result, content, diagnostics: [...(result.diagnostics ?? []), note] },
370
+ truncated: { bytes: droppedBytes, lines: droppedLines },
371
+ };
372
+ }
373
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1,11 @@
1
+ import { type Op } from "@earendil-works/chord/delta";
2
+ /** Temporary compatibility surface for Pico3's flush-based document handling. */
3
+ export interface Tracker<T extends object> {
4
+ readonly state: T;
5
+ readonly target: T;
6
+ readonly dirty: boolean;
7
+ flush(): Op[];
8
+ rebase(): void;
9
+ }
10
+ export declare function track<T extends object>(initial: T): Tracker<T>;
11
+ //# sourceMappingURL=legacy-tracker.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { track as trackChord, } from "@earendil-works/chord/delta";
2
+ class LegacyTracker {
3
+ constructor(initial) {
4
+ this.forceBase = true;
5
+ this.tracker = trackChord(initial);
6
+ }
7
+ get state() {
8
+ this.change ??= this.tracker.beginChange();
9
+ return this.change.state;
10
+ }
11
+ get target() {
12
+ return this.tracker.value;
13
+ }
14
+ get dirty() {
15
+ return this.forceBase || this.change !== undefined;
16
+ }
17
+ flush() {
18
+ let prepared;
19
+ if (this.change !== undefined) {
20
+ const change = this.change;
21
+ this.change = undefined;
22
+ prepared = change.prepare();
23
+ // Pico3 adopts before storage. A storage failure faults the owning Session.
24
+ this.tracker.adopt(prepared);
25
+ }
26
+ if (this.forceBase) {
27
+ this.forceBase = false;
28
+ return [["r", this.tracker.value]];
29
+ }
30
+ return (prepared?.ops ?? []);
31
+ }
32
+ rebase() {
33
+ this.forceBase = true;
34
+ }
35
+ }
36
+ export function track(initial) {
37
+ return new LegacyTracker(initial);
38
+ }
39
+ //# sourceMappingURL=legacy-tracker.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A transaction-scoped revocable membrane over a Chord-tracked document.
3
+ *
4
+ * Every object reached through a document proxy is wrapped lazily; all wrappers of one
5
+ * transaction share one liveness flag; identity is preserved by a WeakMap. Mutations forward
6
+ * to the underlying Chord proxy so its tracker records ops. At transaction finish — success,
7
+ * callback failure, validation failure, storage failure — `revoke()` flips the flag and every
8
+ * retained wrapper, root or nested, throws on any operation.
9
+ *
10
+ * Assigning one wrapper into another (`a.list = b.list`) is rejected: Chord stores the value it
11
+ * is given, and a stored proxy is the classic footgun. Assign plain values; splice in place.
12
+ */
13
+ export declare class Membrane {
14
+ private alive;
15
+ private readonly wrappers;
16
+ private readonly isWrapper;
17
+ private readonly what;
18
+ constructor(what: string);
19
+ revoke(): void;
20
+ wrap<T extends object>(target: T): T;
21
+ private assertPlainInput;
22
+ }
23
+ //# sourceMappingURL=membrane.d.ts.map
@@ -0,0 +1,140 @@
1
+ /**
2
+ * A transaction-scoped revocable membrane over a Chord-tracked document.
3
+ *
4
+ * Every object reached through a document proxy is wrapped lazily; all wrappers of one
5
+ * transaction share one liveness flag; identity is preserved by a WeakMap. Mutations forward
6
+ * to the underlying Chord proxy so its tracker records ops. At transaction finish — success,
7
+ * callback failure, validation failure, storage failure — `revoke()` flips the flag and every
8
+ * retained wrapper, root or nested, throws on any operation.
9
+ *
10
+ * Assigning one wrapper into another (`a.list = b.list`) is rejected: Chord stores the value it
11
+ * is given, and a stored proxy is the classic footgun. Assign plain values; splice in place.
12
+ */
13
+ export class Membrane {
14
+ constructor(what) {
15
+ this.alive = true;
16
+ this.wrappers = new WeakMap();
17
+ this.isWrapper = new WeakSet();
18
+ this.what = what;
19
+ }
20
+ revoke() {
21
+ this.alive = false;
22
+ }
23
+ wrap(target) {
24
+ const hit = this.wrappers.get(target);
25
+ if (hit !== undefined)
26
+ return hit;
27
+ const m = this;
28
+ const dead = () => new TypeError(`document proxy (${m.what}) used outside its transaction`);
29
+ const out = (v) => (typeof v === "object" && v !== null ? m.wrap(v) : v);
30
+ const inp = (value) => {
31
+ if (typeof value !== "object" || value === null)
32
+ return value;
33
+ m.assertPlainInput(value, new WeakSet());
34
+ return JSON.parse(JSON.stringify(value));
35
+ };
36
+ const proxy = new Proxy(target, {
37
+ get(t, key, receiver) {
38
+ if (!m.alive)
39
+ throw dead();
40
+ const v = Reflect.get(t, key, receiver === proxy ? t : receiver);
41
+ if (typeof v === "function") {
42
+ // Methods run against the tracked target. Callback arguments (array elements and
43
+ // the array itself) must cross back through the membrane before user code sees them.
44
+ return (...args) => {
45
+ if (!m.alive)
46
+ throw dead();
47
+ const safeArgs = args.map((arg) => {
48
+ if (typeof arg !== "function")
49
+ return inp(arg);
50
+ return function (...callbackArgs) {
51
+ if (!m.alive)
52
+ throw dead();
53
+ return Reflect.apply(arg, this, callbackArgs.map(out));
54
+ };
55
+ });
56
+ return out(Reflect.apply(v, t, safeArgs));
57
+ };
58
+ }
59
+ return out(v);
60
+ },
61
+ set(t, key, value) {
62
+ if (!m.alive)
63
+ throw dead();
64
+ if (key === "__proto__")
65
+ throw new TypeError(`document proxy (${m.what}) cannot change prototypes`);
66
+ return Reflect.set(t, key, inp(value));
67
+ },
68
+ deleteProperty(t, key) {
69
+ if (!m.alive)
70
+ throw dead();
71
+ return Reflect.deleteProperty(t, key);
72
+ },
73
+ has(t, key) {
74
+ if (!m.alive)
75
+ throw dead();
76
+ return Reflect.has(t, key);
77
+ },
78
+ ownKeys(t) {
79
+ if (!m.alive)
80
+ throw dead();
81
+ return Reflect.ownKeys(t);
82
+ },
83
+ getOwnPropertyDescriptor(t, key) {
84
+ if (!m.alive)
85
+ throw dead();
86
+ const d = Reflect.getOwnPropertyDescriptor(t, key);
87
+ if (d === undefined)
88
+ return undefined;
89
+ // Descriptors must not leak a raw nested object.
90
+ if ("value" in d)
91
+ d.value = out(d.value);
92
+ return d;
93
+ },
94
+ defineProperty(t, key, descriptor) {
95
+ if (!m.alive)
96
+ throw dead();
97
+ if ("get" in descriptor || "set" in descriptor)
98
+ throw new TypeError(`document proxy (${m.what}) cannot define accessors`);
99
+ const safe = "value" in descriptor ? { ...descriptor, value: inp(descriptor.value) } : descriptor;
100
+ return Reflect.defineProperty(t, key, safe);
101
+ },
102
+ getPrototypeOf(t) {
103
+ if (!m.alive)
104
+ throw dead();
105
+ return Reflect.getPrototypeOf(t);
106
+ },
107
+ setPrototypeOf() {
108
+ if (!m.alive)
109
+ throw dead();
110
+ throw new TypeError(`document proxy (${m.what}) cannot change prototypes`);
111
+ },
112
+ isExtensible(t) {
113
+ if (!m.alive)
114
+ throw dead();
115
+ return Reflect.isExtensible(t);
116
+ },
117
+ preventExtensions() {
118
+ if (!m.alive)
119
+ throw dead();
120
+ throw new TypeError(`document proxy (${m.what}) cannot change extensibility`);
121
+ },
122
+ });
123
+ this.wrappers.set(target, proxy);
124
+ this.isWrapper.add(proxy);
125
+ return proxy;
126
+ }
127
+ assertPlainInput(value, seen) {
128
+ if (this.isWrapper.has(value))
129
+ throw new TypeError(`assigning a document proxy into a document (${this.what}); assign a plain value`);
130
+ if (seen.has(value))
131
+ throw new TypeError(`assigning a cyclic value into a document (${this.what})`);
132
+ seen.add(value);
133
+ for (const nested of Object.values(value)) {
134
+ if (typeof nested === "object" && nested !== null)
135
+ this.assertPlainInput(nested, seen);
136
+ }
137
+ seen.delete(value);
138
+ }
139
+ }
140
+ //# sourceMappingURL=membrane.js.map
@@ -0,0 +1,53 @@
1
+ import type { Context } from "@earendil-works/chord";
2
+ import { type Op } from "@earendil-works/chord/delta";
3
+ import type { Conversation, DocRef, Entry, EntryScan, Id, Input, JsonObject, Seq, Storage, Task, TaskScan, Write } from "./types.ts";
4
+ /**
5
+ * In-memory Storage. The reference backend, and the read path of JsonlStorage.
6
+ *
7
+ * A batch is validated and staged in full before any table changes: one failed
8
+ * batch changes no table, no document, no ID high-water, no sequence. Committed
9
+ * IDs are never reused; IDs minted but never committed may be reused after reopen.
10
+ */
11
+ export declare class MemoryStorage implements Storage {
12
+ protected readonly conversationsById: Map<number, Conversation>;
13
+ protected readonly entriesById: Map<number, Entry>;
14
+ protected readonly entriesByConversation: Map<number, Entry[]>;
15
+ protected readonly tasksById: Map<number, Task<import("@earendil-works/chord").JsonValue, import("./types.ts").Checkpoint>>;
16
+ protected readonly inputsById: Map<number, Input>;
17
+ protected readonly inputsByRequest: Map<string, Input>;
18
+ /** Rewindable docs keep their full op history with the seq of each batch, for `docAsOf`. */
19
+ protected readonly rewindableLog: Map<number, {
20
+ seq: Seq;
21
+ ops: Op[];
22
+ }[]>;
23
+ protected readonly stickyLog: Map<number, Op[][]>;
24
+ protected sessionDoc: JsonObject | undefined;
25
+ protected readonly entrySeq: Map<number, number>;
26
+ protected nextIdValue: number;
27
+ protected seq: Seq;
28
+ private closed;
29
+ mintId(): Id;
30
+ protected setNextId(n: Id): void;
31
+ commit(writes: readonly Write[], _ctx: Context): Promise<Seq>;
32
+ /** Validate the whole batch against the current tables; return a function that applies it. */
33
+ protected stage(writes: readonly Write[], seq: Seq): () => void;
34
+ conversation(id: Id, _ctx?: Context): Promise<Conversation | undefined>;
35
+ conversations(_ctx?: Context): Promise<Conversation[]>;
36
+ entries(ids: readonly Id[], _ctx?: Context): Promise<Map<number, Entry>>;
37
+ /** Newest-first, fork-aware: after this conversation's own entries, the parent's up to the fork point, and so on. */
38
+ scanEntries(scan: EntryScan, _ctx?: Context): Promise<Entry[]>;
39
+ task(id: Id, _ctx?: Context): Promise<Task<import("@earendil-works/chord").JsonValue, import("./types.ts").Checkpoint> | undefined>;
40
+ scanTasks(scan: TaskScan, _ctx?: Context): Promise<Task<import("@earendil-works/chord").JsonValue, import("./types.ts").Checkpoint>[]>;
41
+ input(id: Id, _ctx?: Context): Promise<Input | undefined>;
42
+ inputByRequest(conversationId: Id, requestId: string, _ctx?: Context): Promise<Input | undefined>;
43
+ doc(ref: DocRef, _ctx?: Context): Promise<JsonObject | undefined>;
44
+ /**
45
+ * Commit-granular history: the rewindable state after the atomic commit that contains
46
+ * entry `at`, walking the fork chain for entries owned by ancestors.
47
+ */
48
+ docAsOf(conversationId: Id, at: Id, _ctx?: Context): Promise<JsonObject | undefined>;
49
+ protected fold(log: Op[][]): JsonObject;
50
+ truncate(ref: DocRef, _ctx?: Context): Promise<void>;
51
+ close(_ctx?: Context): Promise<void>;
52
+ }
53
+ //# sourceMappingURL=memory.d.ts.map