@xandreed/core 0.13.0 → 0.14.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.
Files changed (148) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance.functions.d.ts +34 -0
  3. package/dist/conformance.functions.js +25 -0
  4. package/dist/domain/bash.entity.d.ts +82 -0
  5. package/dist/domain/bash.entity.functions.d.ts +17 -0
  6. package/dist/domain/bash.entity.functions.js +26 -0
  7. package/dist/domain/bash.entity.functions.test.d.ts +1 -0
  8. package/dist/domain/bash.entity.js +30 -0
  9. package/dist/domain/file-system.entity.d.ts +91 -0
  10. package/dist/domain/file-system.entity.functions.d.ts +18 -0
  11. package/dist/domain/file-system.entity.functions.js +30 -0
  12. package/dist/domain/file-system.entity.js +37 -0
  13. package/dist/domain/loop-event.entity.d.ts +9 -0
  14. package/dist/domain/media.entity.d.ts +79 -0
  15. package/dist/domain/media.entity.functions.d.ts +76 -0
  16. package/dist/domain/media.entity.functions.js +110 -0
  17. package/dist/domain/media.entity.js +38 -0
  18. package/dist/domain/media.entity.test.d.ts +1 -0
  19. package/dist/domain/message.entity.d.ts +157 -5
  20. package/dist/domain/message.entity.js +8 -4
  21. package/dist/domain/model-catalog.entity.d.ts +2 -0
  22. package/dist/domain/model-catalog.entity.js +2 -1
  23. package/dist/domain/token-usage.entity.d.ts +45 -1
  24. package/dist/domain/token-usage.entity.functions.d.ts +30 -1
  25. package/dist/domain/token-usage.entity.functions.js +24 -4
  26. package/dist/domain/token-usage.entity.js +19 -2
  27. package/dist/domain/token-usage.entity.test.d.ts +1 -0
  28. package/dist/harness/capability.entity.functions.js +2 -1
  29. package/dist/harness/plugin.adapter.d.ts +3 -0
  30. package/dist/harness/plugin.adapter.js +7 -1
  31. package/dist/harness/skill-files.entity.d.ts +101 -0
  32. package/dist/harness/skill-files.entity.functions.d.ts +58 -0
  33. package/dist/harness/skill-files.entity.functions.js +150 -0
  34. package/dist/harness/skill-files.entity.functions.test.d.ts +1 -0
  35. package/dist/harness/skill-files.entity.js +25 -0
  36. package/dist/index.d.ts +48 -8
  37. package/dist/index.js +67 -6
  38. package/dist/loop/mapping.d.ts +12 -6
  39. package/dist/loop/mapping.js +30 -6
  40. package/dist/loop/stepContext.d.ts +8 -0
  41. package/dist/loop/stepContext.js +1 -1
  42. package/dist/loop/traceContent.d.ts +29 -0
  43. package/dist/loop/traceContent.js +4 -0
  44. package/dist/loop/traceContent.test.d.ts +1 -0
  45. package/dist/memory/memory-log.entity.d.ts +152 -14
  46. package/dist/memory/memory-log.entity.functions.d.ts +35 -5
  47. package/dist/memory/memory-log.entity.functions.js +59 -20
  48. package/dist/memory/memory-log.entity.js +19 -6
  49. package/dist/memory/memory-session.js +10 -6
  50. package/dist/memory/memory.conformance.d.ts +8 -1
  51. package/dist/memory/memory.conformance.js +32 -0
  52. package/dist/models/auth-store.conformance.d.ts +13 -0
  53. package/dist/models/auth-store.conformance.js +73 -0
  54. package/dist/models/auth-store.conformance.test.d.ts +1 -0
  55. package/dist/models/model-catalog.conformance.d.ts +15 -0
  56. package/dist/models/model-catalog.conformance.js +27 -0
  57. package/dist/models/model-catalog.conformance.test.d.ts +1 -0
  58. package/dist/models/settings-store.conformance.d.ts +26 -0
  59. package/dist/models/settings-store.conformance.js +80 -0
  60. package/dist/models/settings-store.conformance.test.d.ts +1 -0
  61. package/dist/ports/activity.port.d.ts +35 -0
  62. package/dist/ports/activity.port.js +7 -0
  63. package/dist/ports/auth-store.port.d.ts +7 -5
  64. package/dist/ports/bash.port.d.ts +22 -0
  65. package/dist/ports/bash.port.js +4 -0
  66. package/dist/ports/capability.port.d.ts +11 -1
  67. package/dist/ports/file-system.port.d.ts +44 -15
  68. package/dist/ports/file-system.port.js +1 -7
  69. package/dist/ports/memory.port.d.ts +13 -1
  70. package/dist/ports/model-catalog.port.d.ts +4 -1
  71. package/dist/ports/run-context.port.d.ts +6 -1
  72. package/dist/ports/session-log.port.d.ts +4 -1
  73. package/dist/ports/sessions.port.d.ts +35 -6
  74. package/dist/ports/settings-store.port.d.ts +11 -7
  75. package/dist/ports/step-loop.port.d.ts +30 -2
  76. package/dist/ports/tasks.port.d.ts +5 -1
  77. package/dist/ports/turn-admission.port.d.ts +8 -2
  78. package/dist/ports/turn-events.port.d.ts +8 -1
  79. package/dist/ports/turn-scope.port.d.ts +19 -1
  80. package/dist/ports/turn.port.d.ts +70 -5
  81. package/dist/ports/utility-llm.port.d.ts +2 -0
  82. package/dist/session/activity.entity.d.ts +42 -0
  83. package/dist/session/activity.entity.functions.d.ts +21 -0
  84. package/dist/session/activity.entity.functions.js +23 -0
  85. package/dist/session/activity.entity.functions.test.d.ts +1 -0
  86. package/dist/session/activity.entity.js +15 -0
  87. package/dist/session/chassis.js +2 -1
  88. package/dist/session/session-event.entity.d.ts +68 -6
  89. package/dist/session/session-event.entity.functions.d.ts +34 -2
  90. package/dist/session/session-event.entity.functions.js +54 -16
  91. package/dist/session/session-event.entity.js +19 -2
  92. package/dist/session/session-log.conformance.d.ts +4 -3
  93. package/dist/session/session-log.conformance.js +16 -14
  94. package/dist/session/session-log.recovery.conformance.d.ts +40 -0
  95. package/dist/session/session-log.recovery.conformance.js +143 -0
  96. package/dist/session/session-log.recovery.conformance.test.d.ts +1 -0
  97. package/dist/session/sessions.conformance.d.ts +79 -0
  98. package/dist/session/sessions.conformance.js +355 -0
  99. package/dist/session/sessions.entity.d.ts +36 -3
  100. package/dist/session/sessions.entity.functions.d.ts +42 -0
  101. package/dist/session/sessions.entity.functions.js +26 -0
  102. package/dist/session/sessions.entity.js +17 -4
  103. package/dist/session/sessions.entity.test.d.ts +1 -0
  104. package/dist/session/tasks.conformance.d.ts +70 -0
  105. package/dist/session/tasks.conformance.js +213 -0
  106. package/dist/session/tasks.conformance.test.d.ts +1 -0
  107. package/dist/turn/model-request.entity.d.ts +4 -0
  108. package/dist/turn/model-request.entity.js +7 -2
  109. package/dist/turn/step-loop.conformance.d.ts +4 -1
  110. package/dist/turn/step-loop.conformance.js +73 -7
  111. package/dist/turn/tool-registry.conformance.d.ts +38 -0
  112. package/dist/turn/tool-registry.conformance.js +206 -0
  113. package/dist/turn/turn-bus.js +4 -3
  114. package/dist/turn/turn-event.entity.d.ts +134 -34
  115. package/dist/turn/turn-event.entity.functions.d.ts +11 -0
  116. package/dist/turn/turn-event.entity.functions.js +9 -1
  117. package/dist/turn/turn-event.entity.js +76 -3
  118. package/dist/turn/turn-event.entity.test.d.ts +1 -0
  119. package/dist/turn/turn-lifecycle.d.ts +46 -8
  120. package/dist/turn/turn-lifecycle.js +33 -3
  121. package/dist/turn/turn-lifecycle.test.d.ts +1 -0
  122. package/dist/turn/turn-run.d.ts +15 -3
  123. package/dist/turn/turn-run.js +37 -9
  124. package/dist/turn/turn.adapter.d.ts +6 -5
  125. package/dist/turn/turn.adapter.js +15 -3
  126. package/dist/turn/turn.conformance.d.ts +11 -2
  127. package/dist/turn/turn.conformance.js +63 -2
  128. package/dist/turn/user-message.entity.d.ts +14 -4
  129. package/dist/turn/user-message.entity.functions.d.ts +21 -0
  130. package/dist/turn/user-message.entity.functions.js +24 -0
  131. package/dist/turn/user-message.entity.js +6 -8
  132. package/dist/turn/user-message.entity.test.d.ts +1 -0
  133. package/dist/util/glob.d.ts +30 -0
  134. package/dist/util/glob.js +91 -0
  135. package/dist/util/glob.test.d.ts +1 -0
  136. package/dist/workspace/bash.conformance.d.ts +37 -0
  137. package/dist/workspace/bash.conformance.js +120 -0
  138. package/dist/workspace/bash.conformance.test.d.ts +1 -0
  139. package/dist/workspace/file-system.conformance.d.ts +38 -0
  140. package/dist/workspace/file-system.conformance.js +254 -0
  141. package/dist/workspace/file-system.conformance.test.d.ts +1 -0
  142. package/dist/workspace/file-system.memory.adapter.d.ts +23 -0
  143. package/dist/workspace/file-system.memory.adapter.js +132 -0
  144. package/package.json +1 -1
  145. package/dist/harness/skill-files.d.ts +0 -17
  146. package/dist/harness/skill-files.js +0 -25
  147. package/dist/ports/shell.port.d.ts +0 -32
  148. package/dist/ports/shell.port.js +0 -16
@@ -0,0 +1,355 @@
1
+ import { Deferred, Effect, Exit, Fiber, Option, Ref, Scope } from "effect";
2
+ import { check, expect, freshId, kindsOf, pixelPng, settles } from "../conformance.functions.js";
3
+ import { mediaFromBase64 } from "../domain/media.entity.functions.js";
4
+ import { canonicalJson } from "../memory/memory-log.entity.functions.js";
5
+ import { TurnAdmission } from "../ports/turn-admission.port.js";
6
+ import { UserMessage } from "../turn/user-message.entity.js";
7
+ import { TurnRefused } from "./sessions.entity.js";
8
+ const admitAll = TurnAdmission.of({ admit: (_turn, open) => open }), LONG = 600000, SHORT = 1000, say = (text, key = text, command = {}) => ({ _tag: "User", userMessage: new UserMessage({ text }), runId: `run-${key}`, key, command }), resume = (turn, key = `resume:${turn}`) => ({ _tag: "User", userMessage: new UserMessage({ text: "Continue." }), runId: `run-${key}`, key, command: {}, resumes: turn }), completed = { reason: "completed", failure: Option.none() }, item = (id) => ({ id, source: { kind: "conformance", id }, content: `the result of ${id}` }), turnOf = (event) => Option.getOrElse(event.turn, () => 0), codeOf = (event) => {
9
+ const failure = event.data.failure;
10
+ return typeof failure === "object" && failure !== null && "code" in failure && typeof failure.code === "string" ? failure.code : "none";
11
+ }, endings = (events) => events.filter((event) => event.kind === "turn.ended").map((event) => `${turnOf(event)}:${String(event.data.reason)}:${codeOf(event)}`).join(), created = (sessions, owner) => sessions.create({ owner }).pipe(Effect.map((view) => ({ id: view.header.id, owner }))), openTurn = (sessions, address) => sessions.get(address).pipe(Effect.map((view) => Option.map(view.open, (open) => open.turn))), refusal = (sessions, address, input) => Effect.scoped(sessions.begin(address, input)).pipe(Effect.match({
12
+ onFailure: (error) => error._tag === "NotResumable" ? `NotResumable:${error.reason}` : error._tag,
13
+ onSuccess: (writer) => `opened turn ${writer.admitted.turn}`
14
+ })), writeRefused = (writer) => writer.append([{ kind: "conformance.late", data: {} }]).pipe(Effect.andThen(writer.flush), Effect.match({ onFailure: (error) => error.code, onSuccess: () => "stored" })), scopeOfCheck = Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void));
15
+ export const sessionsConformance = (subject) => {
16
+ const scoped = (name, id, body) => check(name, id, (owner) => Effect.scoped(body(owner))), leased = (ttlMs = LONG, admission = admitAll) => subject.leased({ ttlMs, admission });
17
+ return [
18
+ scoped("a session is created empty, read back by its owner only, and its id is created once", "create", (owner) => Effect.gen(function* () {
19
+ const { sessions } = yield* leased(), view = yield* sessions.create({ owner, origin: "conformance", meta: { purpose: "kit", nested: { list: [1, "two", null] } } }), address = { id: view.header.id, owner };
20
+ yield* expect("create", view.header.owner === owner && view.header.origin === "conformance" && Option.isNone(view.header.parent) && canonicalJson(view.header.meta) === canonicalJson({ purpose: "kit", nested: { list: [1, "two", null] } }), `the header was created as ${canonicalJson({ ...view.header, parent: null })}`);
21
+ yield* expect("create", view.turns === 0 && Option.isNone(view.open) && view.pending === 0, `a new session shows ${view.turns} turns, ${view.pending} waiting, ${Option.isSome(view.open) ? "one open" : "none open"}`);
22
+ const read = yield* sessions.get(address);
23
+ yield* expect("create", read.header.id === view.header.id && read.turns === 0, "get does not read the session as it was created");
24
+ const again = yield* Effect.flip(sessions.create({ owner, id: view.header.id }));
25
+ yield* expect("create", again._tag === "SessionExists", `creating an existing id gave ${again._tag}`);
26
+ const stranger = { ...address, owner: `${owner}-stranger` }, refused = [
27
+ yield* Effect.flip(sessions.get(stranger)),
28
+ yield* Effect.flip(sessions.read(stranger)),
29
+ yield* Effect.flip(sessions.cancel(stranger, 1)),
30
+ yield* Effect.flip(sessions.get({ id: yield* freshId, owner }))
31
+ ].map((error) => error._tag);
32
+ yield* expect("create", refused.every((tag) => tag === "SessionMissing"), `another owner's address and an unknown id gave ${refused.join()}`);
33
+ yield* expect("create", (yield* sessions.read(address)).length === 0, "a new session holds events");
34
+ })),
35
+ scoped("an owner's sessions are listed newest first in pages, and a session's children only under it", "list", (owner) => Effect.gen(function* () {
36
+ const { sessions } = yield* leased(), first = yield* created(sessions, owner);
37
+ yield* subject.advance(10);
38
+ const second = yield* created(sessions, owner);
39
+ yield* subject.advance(10);
40
+ const third = yield* created(sessions, owner), child = yield* sessions.fork(first, { origin: "conformance", inherit: !1 }), page = yield* sessions.list({ owner, limit: 2 });
41
+ yield* expect("list", page.sessions.map((view) => view.header.id).join() === [third.id, second.id].join() && Option.isSome(page.next), `the first page of two lists ${page.sessions.length} sessions${Option.isSome(page.next) ? "" : " and no next page"}`);
42
+ const rest = yield* sessions.list({ owner, limit: 2, ...Option.match(page.next, { onNone: () => ({}), onSome: (before) => ({ before }) }) });
43
+ yield* expect("list", rest.sessions.map((view) => view.header.id).join() === first.id && Option.isNone(rest.next), `the page after it lists ${rest.sessions.length} sessions${Option.isSome(rest.next) ? " and another page" : ""}`);
44
+ const children = yield* sessions.list({ owner, parent: first.id });
45
+ yield* expect("list", children.sessions.map((view) => view.header.id).join() === child.header.id, `the parent's children are ${children.sessions.length}`);
46
+ const others = yield* sessions.list({ owner: `${owner}-stranger` });
47
+ yield* expect("list", others.sessions.length === 0, "another owner lists these sessions");
48
+ })),
49
+ scoped("a fork's history is its parent's up to the last closed turn and its turns count on from there; a spawn starts empty", "fork", (owner) => Effect.gen(function* () {
50
+ const { sessions } = yield* leased(), parent = yield* created(sessions, owner);
51
+ yield* Effect.scoped(sessions.begin(parent, say("one")).pipe(Effect.tap((writer) => writer.append([{ kind: "conformance.answer", data: { n: 1 } }]).pipe(Effect.andThen(writer.end(completed))))));
52
+ const left = yield* scopeOfCheck, open = yield* sessions.begin(parent, say("two")).pipe(Scope.provide(left));
53
+ yield* open.append([{ kind: "conformance.answer", data: { n: 2 } }]);
54
+ yield* open.flush;
55
+ const fork = yield* sessions.fork(parent, { origin: "conformance", inherit: !0, meta: { task: "t1" } }), closing = (yield* sessions.read(parent, { kinds: ["turn.ended"] })).at(-1), lineage = fork.header.parent;
56
+ yield* expect("fork", Option.exists(lineage, (at) => at.id === parent.id && at.turnAtFork === 1 && at.through === (closing?.seq ?? -1)) && fork.turns === 1 && fork.header.owner === owner && fork.header.origin === "conformance" && canonicalJson(fork.header.meta) === canonicalJson({ task: "t1" }), `the fork's lineage is ${canonicalJson(Option.getOrNull(lineage))} with ${fork.turns} turns`);
57
+ const child = { id: fork.header.id, owner }, writer = yield* sessions.begin(child, say("in the fork"));
58
+ yield* expect("fork", writer.admitted.turn === 2, `the fork's first turn is ${writer.admitted.turn}, not 2`);
59
+ const history = kindsOf(yield* writer.history([]));
60
+ yield* expect("fork", history === "turn.started@1,conformance.answer@1,turn.ended@1", `the fork's history is ${history}`);
61
+ yield* writer.append([{ kind: "conformance.answer", data: { n: 3 } }]);
62
+ const snapshot = kindsOf(yield* writer.snapshot(["conformance.answer"]));
63
+ yield* expect("fork", snapshot === "conformance.answer@1,conformance.answer@2", `the fork's snapshot is ${snapshot}`);
64
+ const spawn = yield* sessions.fork(parent, { origin: "conformance", inherit: !1 }), fresh = yield* sessions.begin({ id: spawn.header.id, owner }, say("from nothing"));
65
+ yield* expect("fork", spawn.turns === 0 && fresh.admitted.turn === 1 && (yield* fresh.history([])).length === 0, `a spawn starts at ${spawn.turns} turns`);
66
+ const orphan = yield* Effect.flip(sessions.fork({ id: yield* freshId, owner }, { origin: "conformance", inherit: !0 }));
67
+ yield* expect("fork", orphan._tag === "SessionMissing", `forking a missing session gave ${orphan._tag}`);
68
+ })),
69
+ scoped("one turn is open per session: a second begin is SessionBusy on any instance, which also sees the turn open", "busy", (owner) => Effect.gen(function* () {
70
+ const [one, two] = [yield* leased(), yield* leased()], address = yield* created(one.sessions, owner), writer = yield* one.sessions.begin(address, say("first", "k1", { canvas: "c1" })), admitted = writer.admitted;
71
+ yield* expect("busy", admitted.turn === 1 && admitted.key === "k1" && admitted.runId === "run-k1" && admitted.origin === "user" && admitted.userMessage.text === "first" && canonicalJson(admitted.command) === canonicalJson({ canvas: "c1" }) && admitted.claimed.length === 0 && admitted.session.id === address.id && writer.started.kind === "turn.started" && Option.contains(writer.started.turn, 1), `the turn was admitted as ${canonicalJson({ ...admitted, userMessage: admitted.userMessage.text })}`);
72
+ const views = [yield* one.sessions.get(address), yield* two.sessions.get(address)];
73
+ yield* expect("busy", views.every((view) => Option.exists(view.open, (open) => open.turn === 1 && open.key === "k1" && open.origin === "user") && view.turns === 1), "an instance does not show the open turn");
74
+ const busy = [yield* Effect.flip(Effect.scoped(one.sessions.begin(address, say("second", "k2")))), yield* Effect.flip(Effect.scoped(two.sessions.begin(address, say("second", "k2"))))];
75
+ yield* expect("busy", busy.every((error) => error._tag === "SessionBusy" && error.turn === 1), `a second begin gave ${busy.map((error) => error._tag).join()}`);
76
+ yield* writer.end(completed);
77
+ const next = yield* two.sessions.begin(address, say("second", "k2"));
78
+ yield* expect("busy", next.admitted.turn === 2 && Option.contains(yield* openTurn(one.sessions, address), 2), `the begin after the end opened turn ${next.admitted.turn}`);
79
+ const started = kindsOf(yield* one.sessions.read(address, { kinds: ["turn.started"] }));
80
+ yield* expect("busy", started === "turn.started@1,turn.started@2", `the session stored ${started}`);
81
+ })),
82
+ scoped("of begins racing on one session from several instances, exactly one opens a turn", "race", (owner) => Effect.gen(function* () {
83
+ const instances = [yield* leased(), yield* leased(), yield* leased()], address = yield* created(instances[0].sessions, owner), attempts = yield* Effect.forEach(instances, (instance, n) => Effect.result(instance.sessions.begin(address, say(`racer ${n}`))), { concurrency: "unbounded" }), won = attempts.filter((attempt) => attempt._tag === "Success").length, busy = attempts.filter((attempt) => attempt._tag === "Failure" && attempt.failure._tag === "SessionBusy").length;
84
+ yield* expect("race", won === 1 && busy === 2, `of three racing begins ${won} opened a turn and ${busy} were busy`);
85
+ const started = (yield* instances[1].sessions.read(address, { kinds: ["turn.started"] })).length;
86
+ yield* expect("race", started === 1, `the race stored ${started} turns`);
87
+ })),
88
+ scoped("a key's retry is the same turn (TurnDuplicate, whatever its meta) and anything else under the key a KeyConflict", "duplicate", (owner) => Effect.gen(function* () {
89
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), asked = (text, canvas, meta) => meta === void 0 ? say(text, "k1", { canvas }) : { ...say(text, "k1", { canvas }), meta }, writer = yield* sessions.begin(address, asked("find alpha", "c1", { request: "r1" })), retries = [
90
+ yield* Effect.flip(Effect.scoped(sessions.begin(address, asked("find alpha", "c1", { request: "r2" })))),
91
+ yield* Effect.flip(Effect.scoped(sessions.begin(address, asked("find alpha", "c1"))))
92
+ ];
93
+ yield* expect("duplicate", retries.every((error) => error._tag === "TurnDuplicate" && error.turn === 1 && error.open), `a retry while the turn is open gave ${retries.map((error) => error._tag === "TurnDuplicate" ? `TurnDuplicate(${error.turn}, open ${error.open})` : error._tag).join()}`);
94
+ const conflicts = [
95
+ yield* Effect.flip(Effect.scoped(sessions.begin(address, asked("find beta", "c1")))),
96
+ yield* Effect.flip(Effect.scoped(sessions.begin(address, asked("find alpha", "c2"))))
97
+ ];
98
+ yield* expect("duplicate", conflicts.every((error) => error._tag === "KeyConflict" && error.key === "k1" && error.turn === 1), `another message or command under the key gave ${conflicts.map((error) => error._tag).join()}`);
99
+ yield* expect("duplicate", Option.contains(yield* sessions.lookup(address, "k1"), 1) && Option.isNone(yield* sessions.lookup(address, "k-unused")), "lookup does not find the key's turn");
100
+ yield* writer.end(completed);
101
+ const after = yield* Effect.flip(Effect.scoped(sessions.begin(address, asked("find alpha", "c1"))));
102
+ yield* expect("duplicate", after._tag === "TurnDuplicate" && !after.open, `a retry after the turn ended gave ${after._tag}${after._tag === "TurnDuplicate" ? `, open ${after.open}` : ""}`);
103
+ const started = kindsOf(yield* sessions.read(address, { kinds: ["turn.started"] }));
104
+ yield* expect("duplicate", started === "turn.started@1", `the retries stored ${started}`);
105
+ })),
106
+ scoped("a turn's meta is stored, admitted and shown to the host's admission, {} when none is given, and the inbox's from drain", "meta", (owner) => Effect.gen(function* () {
107
+ const seen = yield* Ref.make([]), recording = TurnAdmission.of({ admit: (turn, open) => Ref.update(seen, (all) => [...all, canonicalJson(turn.meta)]).pipe(Effect.andThen(open)) }), { sessions } = yield* leased(LONG, recording), address = yield* created(sessions, owner), shown = yield* Effect.scoped(sessions.begin(address, { ...say("one", "k1"), meta: { tenant: "acme", nested: { n: 1 } } }).pipe(Effect.tap((writer) => writer.end(completed)), Effect.map((writer) => [canonicalJson(writer.admitted.meta), canonicalJson(writer.started.data.meta ?? null)])));
108
+ yield* expect("meta", shown.every((json) => json === canonicalJson({ tenant: "acme", nested: { n: 1 } })), `the turn's meta was admitted and stored as ${shown.join(" and ")}`);
109
+ const plain = yield* Effect.scoped(sessions.begin(address, say("two", "k2")).pipe(Effect.tap((writer) => writer.end(completed)), Effect.map((writer) => [canonicalJson(writer.admitted.meta), canonicalJson(writer.started.data.meta ?? {})])));
110
+ yield* expect("meta", plain.every((json) => json === "{}"), `a turn without meta was admitted and stored with ${plain.join(" and ")}`);
111
+ yield* sessions.deliver(address, item("i1"));
112
+ const drained = yield* Ref.make([]);
113
+ yield* sessions.drain(address, (writer) => Ref.set(drained, [canonicalJson(writer.admitted.meta), canonicalJson(writer.started.data.meta ?? null)]), { maxTurns: 1, meta: { source: "drain" } });
114
+ yield* expect("meta", (yield* Ref.get(drained)).join() === [canonicalJson({ source: "drain" }), canonicalJson({ source: "drain" })].join(), `the inbox turn's meta was ${(yield* Ref.get(drained)).join(" and ")}`);
115
+ const admitted = (yield* Ref.get(seen)).join(" ");
116
+ yield* expect("meta", admitted === [canonicalJson({ tenant: "acme", nested: { n: 1 } }), "{}", canonicalJson({ source: "drain" })].join(" "), `the host's admission saw ${admitted}`);
117
+ })),
118
+ scoped("only the last user's turn, cancelled or interrupted, is resumed; every other is NotResumable with why, and nothing opens", "resume", (owner) => Effect.gen(function* () {
119
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner);
120
+ yield* Effect.scoped(sessions.begin(address, say("one")).pipe(Effect.tap((writer) => writer.end(completed))));
121
+ const early = [yield* refusal(sessions, address, resume(1)), yield* refusal(sessions, address, resume(7)), yield* refusal(sessions, address, resume(1.5, "resume:half"))];
122
+ yield* expect("resume", early.join() === "NotResumable:finished,NotResumable:missing,NotResumable:missing", `resuming a completed, an unknown and a fractional turn gave ${early.join()}`);
123
+ yield* sessions.begin(address, say("two"));
124
+ yield* sessions.cancel(address, 2);
125
+ const resumed = yield* Effect.scoped(sessions.begin(address, resume(2)).pipe(Effect.tap((writer) => writer.end(completed)), Effect.map((writer) => [writer.admitted.turn, writer.admitted.resumes ?? null, writer.started.data.resumes ?? null].join())));
126
+ yield* expect("resume", resumed === "3,2,2", `resuming the cancelled turn 2 admitted turn, resumes and stored resumes ${resumed}`);
127
+ const after = [yield* refusal(sessions, address, resume(2)), yield* refusal(sessions, address, resume(2, "resume:2:again"))];
128
+ yield* expect("resume", after.join() === "TurnDuplicate,NotResumable:superseded", `retrying the resume, and resuming again under a new key, gave ${after.join()}`);
129
+ yield* sessions.deliver(address, item("i1"));
130
+ yield* sessions.drain(address, () => Effect.interrupt, { maxTurns: 1 });
131
+ const inbox = yield* refusal(sessions, address, resume(4));
132
+ yield* expect("resume", inbox === "NotResumable:inbox", `resuming an interrupted inbox turn gave ${inbox}`);
133
+ const before = yield* sessions.get(address), busyFirst = yield* Effect.scoped(Effect.gen(function* () {
134
+ yield* sessions.begin(address, say("open"));
135
+ return yield* refusal(sessions, address, resume(4));
136
+ }));
137
+ yield* expect("resume", busyFirst === "SessionBusy", `a resume while a turn is open gave ${busyFirst}`);
138
+ yield* expect("resume", before.turns === 4, `the refused resumes moved the session to ${before.turns} turns`);
139
+ })),
140
+ scoped("a message whose image its bytes do not support is refused with AttachmentRejected and opens nothing; a good one is stored", "attachments", (owner) => Effect.gen(function* () {
141
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), image = yield* mediaFromBase64(pixelPng, { name: "pixel.png" }), asked = (attachments, key) => ({ _tag: "User", userMessage: new UserMessage({ text: "what is this?", attachments }), runId: `run-${key}`, key, command: {} }), rejected = yield* refusal(sessions, address, asked([{ ...image, data: "aGVsbG8gd29ybGQ=" }], "k1"));
142
+ yield* expect("attachments", rejected === "AttachmentRejected", `an image that is text gave ${rejected}`);
143
+ const view = yield* sessions.get(address);
144
+ yield* expect("attachments", view.turns === 0 && Option.isNone(view.open) && (yield* sessions.read(address)).length === 0, "a refused image opened or stored something");
145
+ const writer = yield* sessions.begin(address, asked([image], "k2")), kept = writer.admitted.userMessage.attachments?.map((media) => media.fingerprint).join() ?? "";
146
+ yield* expect("attachments", writer.admitted.turn === 1 && kept === image.fingerprint && JSON.stringify(writer.started.data.userMessage ?? null).includes(image.fingerprint), `a good image was admitted as ${kept || "nothing"}`);
147
+ })),
148
+ scoped("an inbox turn with nothing waiting is NothingPending and opens nothing; a drain then runs none", "nothing-pending", (owner) => Effect.gen(function* () {
149
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), refused = yield* refusal(sessions, address, { _tag: "Inbox", runId: "run-inbox" });
150
+ yield* expect("nothing-pending", refused === "NothingPending", `an inbox turn with nothing waiting gave ${refused}`);
151
+ const ran = yield* Ref.make(0), drained = yield* sessions.drain(address, () => Ref.update(ran, (count) => count + 1));
152
+ yield* expect("nothing-pending", drained.turns === 0 && (yield* Ref.get(ran)) === 0, `a drain with nothing waiting ran ${yield* Ref.get(ran)} turns`);
153
+ const view = yield* sessions.get(address);
154
+ yield* expect("nothing-pending", view.turns === 0 && (yield* sessions.read(address)).length === 0, "nothing waiting still opened or stored something");
155
+ })),
156
+ scoped("a writer stores in order, stamped with its turn; its end stores how the turn ended, an unended scope failed and an interrupted one interrupted", "writer", (owner) => Effect.gen(function* () {
157
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), late = yield* Effect.scoped(Effect.gen(function* () {
158
+ const writer = yield* sessions.begin(address, say("one"));
159
+ yield* writer.append([{ kind: "conformance.a", data: { n: 1 } }, { kind: "conformance.b", data: { n: 2 } }]);
160
+ const written = yield* writer.write(Effect.succeed("in order"));
161
+ yield* expect("writer", written === "in order", "write did not return its operation's value");
162
+ yield* writer.append([{ kind: "conformance.c", data: { n: 3 } }]);
163
+ const snapshot = kindsOf(yield* writer.snapshot(["conformance.a", "conformance.b", "conformance.c"]));
164
+ yield* expect("writer", snapshot === "conformance.a@1,conformance.b@1,conformance.c@1", `the turn's snapshot is ${snapshot}`);
165
+ const ended = yield* writer.end(completed);
166
+ yield* expect("writer", ended.pending === 0, `the end reports ${ended.pending} items waiting`);
167
+ return yield* writeRefused(writer);
168
+ }));
169
+ yield* expect("writer", late === "turn.closed", `a write after the end gave ${late}`);
170
+ yield* Effect.scoped(sessions.begin(address, say("two")).pipe(Effect.tap((writer) => writer.end({ reason: "failed", failure: Option.some({ code: "conformance.failed", message: "the host failed" }) }))));
171
+ yield* Effect.scoped(sessions.begin(address, say("three")));
172
+ const begun = yield* Deferred.make(), held = yield* Effect.forkChild(Effect.scoped(sessions.begin(address, say("four")).pipe(Effect.andThen(Deferred.succeed(begun, void 0)), Effect.andThen(Effect.never))));
173
+ yield* Deferred.await(begun);
174
+ yield* Fiber.interrupt(held);
175
+ const all = yield* sessions.read(address), stored = endings(all);
176
+ yield* expect("writer", stored === "1:completed:none,2:failed:conformance.failed,3:failed:turn.unended,4:interrupted:turn.interrupted", `the turns ended ${stored}`);
177
+ const first = kindsOf(all.filter((event) => turnOf(event) === 1));
178
+ yield* expect("writer", first === "turn.started@1,conformance.a@1,conformance.b@1,conformance.c@1,turn.ended@1", `turn 1 stored ${first}`);
179
+ const view = yield* sessions.get(address);
180
+ yield* expect("writer", view.turns === 4 && Option.isNone(view.open), `after four ended turns the session shows ${view.turns} turns${Option.isSome(view.open) ? " and one open" : ""}`);
181
+ })),
182
+ scoped("transact records outside a turn, and the framework's kinds are refused as ReservedKind", "transact", (owner) => Effect.gen(function* () {
183
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), decided = yield* sessions.transact(address, (view) => Effect.succeed({ drafts: [{ kind: "conformance.note", data: { turns: view.turns } }], result: "noted" }));
184
+ yield* expect("transact", decided.result === "noted" && kindsOf(decided.events) === "conformance.note", `transact returned ${String(decided.result)} with ${kindsOf(decided.events)}`);
185
+ const reserved = yield* Effect.flip(sessions.transact(address, () => Effect.succeed({ drafts: [{ kind: "turn.ended", data: {} }], result: void 0 })));
186
+ yield* expect("transact", reserved._tag === "ReservedKind", `recording turn.ended gave ${reserved._tag}`);
187
+ const all = kindsOf(yield* sessions.read(address));
188
+ yield* expect("transact", all === "conformance.note", `the session holds ${all}`);
189
+ })),
190
+ scoped("a cancel ends exactly the open turn as cancelled and closes its writer, on its own instance at once and on another at its next write", "cancel", (owner) => Effect.gen(function* () {
191
+ const [holder, other] = [yield* leased(), yield* leased()], address = yield* created(holder.sessions, owner), local = yield* holder.sessions.begin(address, say("stop me")), cancelled = yield* holder.sessions.cancel(address, 1);
192
+ yield* expect("cancel", cancelled.cancelled && cancelled.pending === 0, `cancelling the open turn answered ${canonicalJson(cancelled)}`);
193
+ const closed = yield* local.closed;
194
+ yield* expect("cancel", closed.reason === "cancelled" && closed.turn === 1, `the holder's writer was closed as ${closed.reason}`);
195
+ yield* expect("cancel", (yield* writeRefused(local)) === "turn.closed", "the cancelled writer still writes");
196
+ const remote = yield* holder.sessions.begin(address, say("elsewhere"));
197
+ yield* expect("cancel", (yield* other.sessions.cancel(address, 2)).cancelled, "another instance could not cancel the open turn");
198
+ yield* expect("cancel", (yield* writeRefused(remote)) === "turn.closed", "a writer cancelled from another instance still writes");
199
+ const closedRemotely = yield* remote.closed;
200
+ yield* expect("cancel", closedRemotely.reason === "cancelled", `a writer cancelled from another instance was closed as ${closedRemotely.reason}`);
201
+ const third = yield* holder.sessions.begin(address, say("three")), wrong = [yield* other.sessions.cancel(address, 2), yield* other.sessions.cancel(address, 1)];
202
+ yield* expect("cancel", wrong.every((answer) => !answer.cancelled) && Option.contains(yield* openTurn(other.sessions, address), third.admitted.turn), "a cancel naming a turn that is not open closed something");
203
+ const stored = endings(yield* holder.sessions.read(address, { kinds: ["turn.ended"] }));
204
+ yield* expect("cancel", stored === "1:cancelled:none,2:cancelled:none", `the cancels stored ${stored}`);
205
+ })),
206
+ scoped("removing a session closes its open turn and removes its children", "remove", (owner) => Effect.gen(function* () {
207
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), child = yield* sessions.fork(address, { origin: "conformance", inherit: !1 }), writer = yield* sessions.begin(address, say("open"));
208
+ yield* sessions.remove(address);
209
+ const closed = yield* writer.closed;
210
+ yield* expect("remove", closed.reason === "removed", `the open turn's writer was closed as ${closed.reason}`);
211
+ const gone = [yield* Effect.flip(sessions.get(address)), yield* Effect.flip(sessions.get({ id: child.header.id, owner })), yield* Effect.flip(sessions.remove(address))];
212
+ yield* expect("remove", gone.every((error) => error._tag === "SessionMissing"), `after the removal the session, its child and a second removal gave ${gone.map((error) => error._tag).join()}`);
213
+ })),
214
+ scoped("the inbox: an item is delivered once per id, taken by one inbox turn, waits for the open turn, comes back when its turn is interrupted from outside, and is done when its turn is cancelled", "inbox", (owner) => Effect.gen(function* () {
215
+ const { sessions } = yield* leased(), address = yield* created(sessions, owner), delivered = [yield* sessions.deliver(address, item("i1")), yield* sessions.deliver(address, item("i1"))];
216
+ yield* expect("inbox", delivered.map((answer) => `${answer.delivered}:${answer.pending}`).join() === "true:1,false:1", `delivering one item twice answered ${canonicalJson(delivered)}`);
217
+ const taken = yield* Ref.make(Option.none()), drained = yield* sessions.drain(address, (writer) => Ref.set(taken, Option.some(writer.admitted))), turn = yield* Ref.get(taken);
218
+ yield* expect("inbox", drained.turns === 1 && Option.exists(turn, (admitted) => admitted.origin === "inbox" && admitted.claimed.map((claimed) => claimed.id).join() === "i1" && admitted.claimed[0]?.content === item("i1").content && admitted.userMessage.text.includes(item("i1").content)), `the drain ran ${drained.turns} turns, taking ${canonicalJson(Option.getOrNull(Option.map(turn, (admitted) => admitted.claimed)))}`);
219
+ yield* expect("inbox", (yield* sessions.get(address)).pending === 0 && endings(yield* sessions.read(address)) === "1:completed:none", "the inbox turn did not end completed with its item taken");
220
+ yield* expect("inbox", !(yield* sessions.deliver(address, item("i1"))).delivered, "an item delivered again after it was taken is stored again");
221
+ const writer = yield* sessions.begin(address, say("long answer"));
222
+ yield* expect("inbox", (yield* sessions.deliver(address, item("i2"))).pending === 1, "an item delivered during a turn does not wait");
223
+ yield* expect("inbox", (yield* sessions.drain(address, () => Effect.void)).turns === 0, "a drain ran an inbox turn while a turn was open");
224
+ yield* expect("inbox", (yield* writer.end(completed)).pending === 1, "the turn's end does not report the item that waits");
225
+ yield* expect("inbox", (yield* sessions.drain(address, () => Effect.void)).turns === 1, "the drain after the turn did not take the item");
226
+ yield* sessions.deliver(address, item("i3"));
227
+ const entered = yield* Deferred.make(), held = yield* Effect.forkChild(sessions.drain(address, () => Deferred.succeed(entered, void 0).pipe(Effect.andThen(Effect.never))));
228
+ yield* Deferred.await(entered);
229
+ yield* Fiber.interrupt(held);
230
+ const back = yield* sessions.get(address);
231
+ yield* expect("inbox", back.pending === 1 && Option.isNone(back.open), `after its inbox turn was interrupted from outside ${back.pending} items wait${Option.isSome(back.open) ? " and a turn is open" : ""}`);
232
+ yield* expect("inbox", (yield* sessions.drain(address, () => Effect.void)).turns === 1 && (yield* sessions.get(address)).pending === 0, "the item given back was not taken by the next drain");
233
+ const queued = (yield* sessions.read(address, { kinds: ["inbox.queued"] })).length;
234
+ yield* expect("inbox", queued === 3, `three items were stored as ${queued}`);
235
+ yield* sessions.deliver(address, item("i4"));
236
+ const claiming = yield* Deferred.make(), claim = yield* Effect.forkChild(sessions.drain(address, (writer) => Deferred.succeed(claiming, writer.admitted.turn).pipe(Effect.andThen(Effect.never)))), claimTurn = yield* Deferred.await(claiming), stopped = yield* sessions.cancel(address, claimTurn);
237
+ yield* Fiber.interrupt(claim);
238
+ const done = yield* sessions.get(address);
239
+ yield* expect("inbox", stopped.cancelled && stopped.pending === 0 && done.pending === 0 && Option.isNone(done.open), `after its inbox turn was cancelled ${done.pending} items wait${Option.isSome(done.open) ? " and a turn is open" : ""} (a cancelled turn's items are done)`);
240
+ const last = endings(yield* sessions.read(address, { kinds: ["turn.ended"] })).split(",").at(-1);
241
+ yield* expect("inbox", last === `${claimTurn}:cancelled:none`, `the cancelled inbox turn was closed as ${last ?? "nothing"}`);
242
+ yield* expect("inbox", (yield* sessions.drain(address, () => Effect.void)).turns === 0, "a drain took again an item whose turn was cancelled");
243
+ })),
244
+ scoped("a turn whose lease ran out is shown closed, left alone by a drain with nothing waiting, reaped as interrupted by the next begin, and its writer refused", "lease", (owner) => Effect.gen(function* () {
245
+ const [first, second] = [yield* leased(SHORT), yield* leased(SHORT)], address = yield* created(first.sessions, owner), writer = yield* first.sessions.begin(address, say("slow"));
246
+ yield* subject.advance(SHORT / 2);
247
+ yield* expect("lease", Option.contains(yield* openTurn(second.sessions, address), 1), "a turn within its lease is shown closed");
248
+ const early = yield* refusal(second.sessions, address, say("too early"));
249
+ yield* expect("lease", early === "SessionBusy", `a begin within the lease gave ${early}`);
250
+ yield* subject.advance(SHORT);
251
+ yield* expect("lease", Option.isNone(yield* openTurn(second.sessions, address)), "a turn past its lease is still shown open");
252
+ const idle = yield* second.sessions.drain(address, () => Effect.void), unreaped = endings(yield* second.sessions.read(address, { kinds: ["turn.ended"] }));
253
+ yield* expect("lease", idle.turns === 0 && unreaped === "", `a drain with nothing waiting ran ${idle.turns} turns and stored ${unreaped || "no ending"} (only a turn that opens reaps)`);
254
+ const next = yield* second.sessions.begin(address, say("next"));
255
+ yield* expect("lease", next.admitted.turn === 2, `the begin after the lease opened turn ${next.admitted.turn}`);
256
+ const reaped = endings(yield* second.sessions.read(address, { kinds: ["turn.ended"] }));
257
+ yield* expect("lease", reaped === "1:interrupted:turn.lease", `the expired turn was closed as ${reaped || "nothing"}`);
258
+ yield* expect("lease", (yield* writeRefused(writer)) === "turn.closed", "the old holder still writes");
259
+ const closed = yield* writer.closed;
260
+ yield* expect("lease", closed.reason === "interrupted", `the old holder's writer was closed as ${closed.reason}`);
261
+ })),
262
+ scoped("a write renews the lease: a turn written to within its lease is held for a lease from that write, then reaped", "lease-renewal", (owner) => Effect.gen(function* () {
263
+ const [holder, other] = [yield* leased(SHORT), yield* leased(SHORT)], address = yield* created(holder.sessions, owner), writer = yield* holder.sessions.begin(address, say("long turn")), step = SHORT * 0.6;
264
+ yield* subject.advance(step);
265
+ yield* writer.append([{ kind: "conformance.progress", data: { n: 1 } }]);
266
+ yield* writer.flush;
267
+ yield* subject.advance(step);
268
+ yield* expect("lease-renewal", Option.contains(yield* openTurn(other.sessions, address), 1), `a turn written to ${step} ms ago, ${step * 2} ms after its begin, is shown closed: its write did not renew its lease of ${SHORT} ms`);
269
+ const held = yield* refusal(other.sessions, address, say("too early"));
270
+ yield* expect("lease-renewal", held === "SessionBusy", `a begin within the renewed lease gave ${held}`);
271
+ yield* subject.advance(SHORT);
272
+ const next = yield* other.sessions.begin(address, say("next")), reaped = endings(yield* other.sessions.read(address, { kinds: ["turn.ended"] }));
273
+ yield* expect("lease-renewal", next.admitted.turn === 2 && reaped === "1:interrupted:turn.lease", `once the renewed lease ran out the begin opened turn ${next.admitted.turn} and the turn was closed as ${reaped || "nothing"}`);
274
+ const stored = kindsOf(yield* other.sessions.read(address, { kinds: ["conformance.progress"] }));
275
+ yield* expect("lease-renewal", stored === "conformance.progress@1", `the renewing write was stored as ${stored || "nothing"}`);
276
+ })),
277
+ ...Option.match(Option.fromNullishOr(subject.processOwned), {
278
+ onNone: () => [],
279
+ onSome: (processOwned) => [
280
+ scoped("under process ownership a turn whose process stopped is reaped by the next begin, and one whose process runs stays held", "process", (owner) => Effect.gen(function* () {
281
+ const named = (pid, startedAt, running) => processOwned({ self: { host: "conformance-host", pid, startedAt }, running, admission: admitAll }), crashed = yield* named(101, 1, [202]), restarted = yield* named(202, 2, [202]), address = yield* created(crashed.sessions, owner), writer = yield* crashed.sessions.begin(address, say("before the crash"));
282
+ yield* expect("process", Option.isNone(yield* openTurn(restarted.sessions, address)), "a turn whose process stopped is shown open");
283
+ const next = yield* restarted.sessions.begin(address, say("after the restart"));
284
+ yield* expect("process", next.admitted.turn === 2, `the begin after the crash opened turn ${next.admitted.turn}`);
285
+ yield* expect("process", (yield* writeRefused(writer)) === "turn.closed", "the stopped process's writer still writes");
286
+ const reborn = yield* named(202, 3, [202]), third = yield* reborn.sessions.begin(address, say("pid reused"));
287
+ yield* expect("process", third.admitted.turn === 3, `a process reusing the holder's pid opened turn ${third.admitted.turn}`);
288
+ const reaped = endings(yield* reborn.sessions.read(address, { kinds: ["turn.ended"] }));
289
+ yield* expect("process", reaped === "1:interrupted:turn.lease,2:interrupted:turn.lease", `the stopped holders' turns were closed as ${reaped}`);
290
+ const alive = yield* named(303, 3, [303, 404]), observer = yield* named(404, 4, [303, 404]), other = yield* created(alive.sessions, owner);
291
+ yield* alive.sessions.begin(other, say("still running"));
292
+ yield* expect("process", Option.contains(yield* openTurn(observer.sessions, other), 1), "a turn whose process runs is shown closed");
293
+ const busy = yield* refusal(observer.sessions, other, say("second"));
294
+ yield* expect("process", busy === "SessionBusy", `a begin while the holder's process runs gave ${busy}`);
295
+ }))
296
+ ]
297
+ }),
298
+ scoped("TurnActivity agrees with what is open: a begin counts from the moment it is asked for, then its turn per session and turn until its scope closes", "activity", (owner) => Effect.gen(function* () {
299
+ const gate = yield* Deferred.make(), queued = yield* Deferred.make(), refusing = yield* Ref.make(!1), admission = TurnAdmission.of({
300
+ admit: (turn, open) => Effect.gen(function* () {
301
+ if (yield* Ref.get(refusing))
302
+ return yield* Effect.fail(new TurnRefused({ session: turn.session.id, reason: "conformance", message: "the kit refuses it" }));
303
+ if (turn.key === "held at admission")
304
+ yield* Deferred.succeed(queued, void 0).pipe(Effect.andThen(Deferred.await(gate)));
305
+ return yield* open;
306
+ })
307
+ }), { sessions, activity: provided } = yield* leased(LONG, admission);
308
+ if (Option.isNone(provided))
309
+ return;
310
+ const activity = provided.value, other = yield* leased(), [a, b, c] = [yield* created(sessions, owner), yield* created(sessions, owner), yield* created(sessions, owner)], label = (address) => address.id === a.id ? "a" : address.id === b.id ? "b" : "other", current = activity.current.pipe(Effect.map((now) => ({
311
+ open: now.open.map((turn) => `${label({ id: turn.session, owner })}:${turn.turn}`).join(),
312
+ since: now.open.map((turn) => turn.since),
313
+ beginning: now.beginning
314
+ }))), shows = (open, beginning, when) => current.pipe(Effect.flatMap((now) => expect("activity", now.open === open && now.beginning === beginning, `${when}: open [${now.open}], beginning ${now.beginning}; expected open [${open}], beginning ${beginning}`)));
315
+ yield* shows("", 0, "before any begin");
316
+ yield* expect("activity", yield* settles(yield* Effect.forkChild(activity.idle)), "idle does not complete with nothing open");
317
+ const scopeA = yield* scopeOfCheck, beginning = yield* Effect.forkChild(sessions.begin(a, say("held at admission")).pipe(Scope.provide(scopeA)));
318
+ yield* Deferred.await(queued);
319
+ yield* shows("", 1, "a begin waiting for the host's admission");
320
+ const idle = yield* Effect.forkChild(activity.idle);
321
+ yield* expect("activity", !(yield* settles(idle)), "idle completed while a begin was waiting for its admission");
322
+ yield* Deferred.succeed(gate, void 0);
323
+ const first = yield* Fiber.join(beginning);
324
+ yield* shows("a:1", 0, "a turn handed over");
325
+ yield* subject.advance(10);
326
+ const scopeB = yield* scopeOfCheck;
327
+ yield* sessions.begin(b, say("two")).pipe(Scope.provide(scopeB));
328
+ yield* shows("a:1,b:1", 0, "two sessions with a turn open each");
329
+ yield* Effect.scoped(Effect.gen(function* () {
330
+ yield* other.sessions.begin(yield* created(other.sessions, owner), say("elsewhere"));
331
+ yield* shows("a:1,b:1", 0, "with another instance's turn open");
332
+ }));
333
+ yield* first.end(completed);
334
+ yield* shows("a:1,b:1", 0, "an ended turn whose scope is still open");
335
+ yield* subject.advance(10);
336
+ const scopeNext = yield* scopeOfCheck;
337
+ yield* sessions.begin(a, say("three")).pipe(Scope.provide(scopeNext));
338
+ yield* shows("a:1,b:1,a:2", 0, "the session's next turn begun while the ended one's scope is open");
339
+ const since = (yield* current).since;
340
+ yield* expect("activity", since.every((at, index) => index === 0 || at >= since[index - 1]), `the open turns are not oldest first: since ${since.join()}`);
341
+ yield* Scope.close(scopeA, Exit.void);
342
+ yield* shows("b:1,a:2", 0, "after the ended turn's scope closed");
343
+ yield* Ref.set(refusing, !0);
344
+ const refused = yield* refusal(sessions, c, say("refused"));
345
+ yield* expect("activity", refused === "TurnRefused", `a begin the host refuses gave ${refused}`);
346
+ yield* shows("b:1,a:2", 0, "after a begin the host refused");
347
+ yield* Ref.set(refusing, !1);
348
+ yield* expect("activity", !(yield* settles(idle)), "idle completed while turns were open");
349
+ yield* Scope.close(scopeB, Exit.void);
350
+ yield* Scope.close(scopeNext, Exit.void);
351
+ yield* shows("", 0, "after every scope closed");
352
+ yield* expect("activity", yield* settles(idle), "idle did not complete once nothing was open");
353
+ }))
354
+ ];
355
+ };
@@ -105,8 +105,12 @@ export declare const BeginTurn: Schema.Union<readonly [Schema.TaggedStruct<"User
105
105
  /** The host's idempotency key (e.g. the client's command id). */
106
106
  readonly key: Schema.String;
107
107
  readonly command: Schema.$Record<Schema.String, Schema.Unknown>;
108
+ readonly meta: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
109
+ /** The turn this one resumes (see `NotResumable` for which turns can be). */
110
+ readonly resumes: Schema.optionalKey<Schema.Int>;
108
111
  }>, Schema.TaggedStruct<"Inbox", {
109
112
  readonly runId: Schema.String;
113
+ readonly meta: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
110
114
  }>]>;
111
115
  export type BeginTurn = typeof BeginTurn.Type;
112
116
  /** An admitted turn, as the turn's code reads it. */
@@ -129,9 +133,20 @@ export declare const AdmittedTurn: Schema.Struct<{
129
133
  /** What the turn that takes it shows the model, framed by the sender. */
130
134
  readonly content: Schema.NonEmptyString;
131
135
  }>>;
136
+ /** The host's metadata the turn was begun with (`{}` when none was given). */
137
+ readonly meta: Schema.$Record<Schema.String, Schema.Unknown>;
138
+ /** The turn this one resumes, when it resumes one. */
139
+ readonly resumes: Schema.optionalKey<Schema.Int>;
132
140
  }>;
133
141
  export type AdmittedTurn = typeof AdmittedTurn.Type;
134
- /** How a turn ended, as its closer records it. */
142
+ /**
143
+ * How a turn ended, as its closer records it (`turnEndingOf` gives the
144
+ * ending of a turn's exit). A failure's `code` is the error's own code when
145
+ * it has one; otherwise one of the framework's codes (`turn.failed`,
146
+ * `turn.defect`, `turn.interrupted`, `turn.unended`, `turn.lease`), or a
147
+ * code the closer passes to `writer.end` itself (plugin-tasks: `task.turn`,
148
+ * `task.deadline`; a host's own).
149
+ */
135
150
  export declare const TurnEnding: Schema.Struct<{
136
151
  readonly reason: Schema.Literals<readonly ["completed", "partial", "failed", "cancelled", "interrupted"]>;
137
152
  readonly failure: Schema.OptionFromNullOr<Schema.Struct<{
@@ -152,7 +167,7 @@ declare const TurnDuplicate_base: Schema.Class<TurnDuplicate, Schema.TaggedStruc
152
167
  readonly turn: Schema.Int;
153
168
  readonly open: Schema.Boolean;
154
169
  }>, import("effect/Cause").YieldableError>;
155
- /** The key was begun before, with the same message: that turn is the answer. */
170
+ /** The key was begun before with the same message, command and `resumes`: that turn is the answer. */
156
171
  export declare class TurnDuplicate extends TurnDuplicate_base {
157
172
  }
158
173
  declare const KeyConflict_base: Schema.Class<KeyConflict, Schema.TaggedStruct<"KeyConflict", {
@@ -160,9 +175,27 @@ declare const KeyConflict_base: Schema.Class<KeyConflict, Schema.TaggedStruct<"K
160
175
  readonly key: Schema.String;
161
176
  readonly turn: Schema.Int;
162
177
  }>, import("effect/Cause").YieldableError>;
163
- /** The key was begun before with another message or command. */
178
+ /** The key was begun before with another message, command or `resumes`. */
164
179
  export declare class KeyConflict extends KeyConflict_base {
165
180
  }
181
+ declare const NotResumable_base: Schema.Class<NotResumable, Schema.TaggedStruct<"NotResumable", {
182
+ readonly session: Schema.brand<Schema.String, "ConversationId">;
183
+ readonly turn: Schema.Number;
184
+ readonly reason: Schema.Literals<readonly ["missing", "superseded", "finished", "inbox"]>;
185
+ }>, import("effect/Cause").YieldableError>;
186
+ /**
187
+ * A user's turn asked to resume a turn that cannot be resumed: `missing`, no
188
+ * such turn in the session's own log (a fork's inherited turns are its
189
+ * parent's; a `resumes` that is not a whole number of at least 1 names no
190
+ * turn); `inbox`, an inbox turn; `superseded`, a user's turn was begun after
191
+ * it (other than a resume of it that failed, was cancelled or was
192
+ * interrupted); `finished`, it ended completed, partial or failed. Only the
193
+ * last user's turn, ended cancelled or interrupted (a turn reaped because
194
+ * its holder stopped is interrupted), can be resumed. Nothing was opened.
195
+ * `turn` is the `resumes` asked for, as given.
196
+ */
197
+ export declare class NotResumable extends NotResumable_base {
198
+ }
166
199
  declare const NothingPending_base: Schema.Class<NothingPending, Schema.TaggedStruct<"NothingPending", {
167
200
  readonly session: Schema.brand<Schema.String, "ConversationId">;
168
201
  }>, import("effect/Cause").YieldableError>;
@@ -0,0 +1,42 @@
1
+ import { Exit, Option } from "effect";
2
+ import type { TurnEndReason } from "./session-event.entity.js";
3
+ import type { NotResumable, TurnEnding } from "./sessions.entity.js";
4
+ /**
5
+ * How a turn ended, from its exit:
6
+ * - success: the outcome it returned, with no failure;
7
+ * - a typed error (it wins over an interruption in the same cause): failed,
8
+ * with the error's own `code` and message when it has a string `code`, and
9
+ * `turn.failed` with `<_tag>: <message>` (or the error as text) otherwise;
10
+ * - a defect: failed, `turn.defect` (a runner applies
11
+ * `harnessDefectsAsFailures` before taking the exit, so a `HarnessError`
12
+ * carried as a defect keeps its own code);
13
+ * - an interruption only: interrupted, `turn.interrupted`.
14
+ * Messages are cut to 500 characters. A runner whose exit has no outcome
15
+ * maps it first: `turnEndingOf(Exit.map(exit, () => ({ outcome: "completed" })))`.
16
+ */
17
+ export declare const turnEndingOf: (exit: Exit.Exit<{
18
+ readonly outcome: "completed" | "partial" | "failed";
19
+ }, unknown>) => TurnEnding;
20
+ /**
21
+ * One turn of a session as a resume is checked against it: who began it, the
22
+ * turn it resumed (when it resumed one) and, once it ended, how.
23
+ */
24
+ export interface ResumableTurn {
25
+ readonly turn: number;
26
+ readonly origin: "user" | "inbox";
27
+ readonly resumes: Option.Option<number>;
28
+ readonly ended: Option.Option<TurnEndReason>;
29
+ }
30
+ /**
31
+ * Why turn `target` cannot be resumed, or none when it can, from the
32
+ * session's own turns (each `turn.started`, with the turn it resumed and its
33
+ * `turn.ended`'s reason when it has them): `missing` when no turn has that
34
+ * number (a number that is not a whole number of at least 1 never does),
35
+ * `inbox` when an inbox turn has it, `superseded` when a user's turn was
36
+ * begun after it (only inbox turns, and resumes of it that failed, were
37
+ * cancelled or were interrupted, may follow), `finished` unless it ended
38
+ * cancelled or interrupted (a turn reaped because its holder stopped ended
39
+ * interrupted; one with no end yet is open, which `SessionBusy` answers
40
+ * first).
41
+ */
42
+ export declare const resumeRefusal: (target: number, turns: ReadonlyArray<ResumableTurn>) => Option.Option<NotResumable["reason"]>;
@@ -0,0 +1,26 @@
1
+ import { Cause, Exit, Formatter, Option, Predicate, Result } from "effect";
2
+ const MESSAGE_CHARS = 500, clipped = (text) => text.length <= MESSAGE_CHARS ? text : text.slice(0, MESSAGE_CHARS), stringField = (value, key) => Predicate.isObject(value) ? Option.filter(Option.some(value[key]), Predicate.isString) : Option.none(), fieldsOf = (error) => Predicate.isObject(error) ? Formatter.formatJson(Object.fromEntries(Object.entries(error).filter(([key]) => key !== "_tag"))) : "", failureOfError = (error) => Option.match(stringField(error, "code"), {
3
+ onSome: (code) => ({ code, message: Option.getOrElse(stringField(error, "message"), () => Formatter.format(error)) }),
4
+ onNone: () => ({
5
+ code: "turn.failed",
6
+ message: Option.match(stringField(error, "_tag"), {
7
+ onSome: (tag) => `${tag}: ${Option.getOrElse(Option.filter(stringField(error, "message"), (text) => text.length > 0), () => fieldsOf(error))}`,
8
+ onNone: () => typeof error === "string" ? error : Formatter.format(error)
9
+ })
10
+ })
11
+ }), failed = (failure) => ({ reason: "failed", failure: Option.some({ code: failure.code, message: clipped(failure.message) }) });
12
+ export const turnEndingOf = (exit) => Exit.match(exit, {
13
+ onSuccess: (value) => ({ reason: value.outcome, failure: Option.none() }),
14
+ onFailure: (cause) => Option.match(Cause.findErrorOption(cause), {
15
+ onSome: (error) => failed(failureOfError(error)),
16
+ onNone: () => Result.match(Cause.findDefect(cause), {
17
+ onSuccess: (defect) => failed({ code: "turn.defect", message: Formatter.format(defect) }),
18
+ onFailure: () => Cause.hasInterrupts(cause) ? { reason: "interrupted", failure: Option.some({ code: "turn.interrupted", message: "the turn was interrupted" }) } : failed({ code: "turn.failed", message: "the turn failed without an error" })
19
+ })
20
+ })
21
+ });
22
+ const stopped = (ended) => Option.exists(ended, (reason) => reason === "cancelled" || reason === "interrupted"), supersedes = (target) => (later) => later.turn > target && later.origin === "user" && !(Option.contains(later.resumes, target) && (stopped(later.ended) || Option.contains(later.ended, "failed")));
23
+ export const resumeRefusal = (target, turns) => Option.match(Option.fromNullishOr(turns.find((candidate) => candidate.turn === target)), {
24
+ onNone: () => Option.some("missing"),
25
+ onSome: (found) => found.origin === "inbox" ? Option.some("inbox") : turns.some(supersedes(target)) ? Option.some("superseded") : stopped(found.ended) ? Option.none() : Option.some("finished")
26
+ });
@@ -25,14 +25,18 @@ export const SessionAddress = Schema.Struct({ id: ConversationId, owner: Schema.
25
25
  }), SessionPage = Schema.Struct({
26
26
  sessions: Schema.Array(SessionView),
27
27
  next: Schema.OptionFromNullOr(SessionListCursor)
28
- }), BeginTurn = Schema.Union([
28
+ });
29
+ const TurnMeta = Schema.optionalKey(JsonObject), Resumes = Schema.optionalKey(Schema.Int);
30
+ export const BeginTurn = Schema.Union([
29
31
  Schema.TaggedStruct("User", {
30
32
  userMessage: UserMessage,
31
33
  runId: Schema.String,
32
34
  key: Schema.String,
33
- command: JsonObject
35
+ command: JsonObject,
36
+ meta: TurnMeta,
37
+ resumes: Resumes
34
38
  }),
35
- Schema.TaggedStruct("Inbox", { runId: Schema.String })
39
+ Schema.TaggedStruct("Inbox", { runId: Schema.String, meta: TurnMeta })
36
40
  ]), AdmittedTurn = Schema.Struct({
37
41
  session: SessionAddress,
38
42
  turn: Schema.Int,
@@ -41,7 +45,9 @@ export const SessionAddress = Schema.Struct({ id: ConversationId, owner: Schema.
41
45
  origin: Schema.Literals(["user", "inbox"]),
42
46
  userMessage: UserMessage,
43
47
  command: JsonObject,
44
- claimed: Schema.Array(InboxItem)
48
+ claimed: Schema.Array(InboxItem),
49
+ meta: JsonObject,
50
+ resumes: Resumes
45
51
  }), TurnEnding = Schema.Struct({
46
52
  reason: TurnEndReason,
47
53
  failure: Schema.OptionFromNullOr(Schema.Struct({ code: Schema.String, message: Schema.String }))
@@ -67,6 +73,13 @@ export class KeyConflict extends Schema.TaggedError()("KeyConflict", {
67
73
  }) {
68
74
  }
69
75
 
76
+ export class NotResumable extends Schema.TaggedError()("NotResumable", {
77
+ session: ConversationId,
78
+ turn: Schema.Number,
79
+ reason: Schema.Literals(["missing", "superseded", "finished", "inbox"])
80
+ }) {
81
+ }
82
+
70
83
  export class NothingPending extends Schema.TaggedError()("NothingPending", {
71
84
  session: ConversationId
72
85
  }) {
@@ -0,0 +1 @@
1
+ export {};