@earendil-works/pi-durable 1.3.0 → 1.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.
Files changed (257) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +287 -82
  3. package/dist/codemode/codemode.d.ts +4 -4
  4. package/dist/codemode/codemode.d.ts.map +1 -1
  5. package/dist/codemode/codemode.js +21 -16
  6. package/dist/codemode/codemode.js.map +1 -1
  7. package/dist/codemode/models.d.ts.map +1 -1
  8. package/dist/codemode/models.js +6 -4
  9. package/dist/codemode/models.js.map +1 -1
  10. package/dist/documents.d.ts +1 -1
  11. package/dist/documents.d.ts.map +1 -1
  12. package/dist/documents.js.map +1 -1
  13. package/dist/env/index.d.ts +12 -10
  14. package/dist/env/index.d.ts.map +1 -1
  15. package/dist/env/index.js.map +1 -1
  16. package/dist/env/node-watch.d.ts +3 -3
  17. package/dist/env/node-watch.d.ts.map +1 -1
  18. package/dist/env/node-watch.js +18 -16
  19. package/dist/env/node-watch.js.map +1 -1
  20. package/dist/env/node.d.ts +2 -2
  21. package/dist/env/node.d.ts.map +1 -1
  22. package/dist/env/node.js +12 -4
  23. package/dist/env/node.js.map +1 -1
  24. package/dist/errors.d.ts +18 -2
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +26 -3
  27. package/dist/errors.js.map +1 -1
  28. package/dist/harness/agent.d.ts +13 -2
  29. package/dist/harness/agent.d.ts.map +1 -1
  30. package/dist/harness/agent.js +48 -6
  31. package/dist/harness/agent.js.map +1 -1
  32. package/dist/harness/compaction.d.ts +3 -1
  33. package/dist/harness/compaction.d.ts.map +1 -1
  34. package/dist/harness/compaction.js +38 -6
  35. package/dist/harness/compaction.js.map +1 -1
  36. package/dist/harness/context.d.ts +23 -7
  37. package/dist/harness/context.d.ts.map +1 -1
  38. package/dist/harness/context.js +71 -25
  39. package/dist/harness/context.js.map +1 -1
  40. package/dist/harness/define.d.ts +7 -3
  41. package/dist/harness/define.d.ts.map +1 -1
  42. package/dist/harness/define.js +4 -0
  43. package/dist/harness/define.js.map +1 -1
  44. package/dist/harness/generation.d.ts +38 -5
  45. package/dist/harness/generation.d.ts.map +1 -1
  46. package/dist/harness/generation.js +497 -108
  47. package/dist/harness/generation.js.map +1 -1
  48. package/dist/harness/harness.d.ts +7 -2
  49. package/dist/harness/harness.d.ts.map +1 -1
  50. package/dist/harness/harness.js +147 -57
  51. package/dist/harness/harness.js.map +1 -1
  52. package/dist/harness/inbox.d.ts +7 -3
  53. package/dist/harness/inbox.d.ts.map +1 -1
  54. package/dist/harness/inbox.js +7 -4
  55. package/dist/harness/inbox.js.map +1 -1
  56. package/dist/harness/json.d.ts +20 -4
  57. package/dist/harness/json.d.ts.map +1 -1
  58. package/dist/harness/json.js +23 -5
  59. package/dist/harness/json.js.map +1 -1
  60. package/dist/harness/live.d.ts +122 -16
  61. package/dist/harness/live.d.ts.map +1 -1
  62. package/dist/harness/live.js +93 -12
  63. package/dist/harness/live.js.map +1 -1
  64. package/dist/harness/partial-changes.d.ts +54 -0
  65. package/dist/harness/partial-changes.d.ts.map +1 -0
  66. package/dist/harness/partial-changes.js +162 -0
  67. package/dist/harness/partial-changes.js.map +1 -0
  68. package/dist/harness/prompt.d.ts +1 -1
  69. package/dist/harness/prompt.d.ts.map +1 -1
  70. package/dist/harness/prompt.js +1 -1
  71. package/dist/harness/prompt.js.map +1 -1
  72. package/dist/harness/registry-reader.d.ts +11 -0
  73. package/dist/harness/registry-reader.d.ts.map +1 -0
  74. package/dist/harness/registry-reader.js +25 -0
  75. package/dist/harness/registry-reader.js.map +1 -0
  76. package/dist/harness/scheduler.d.ts +86 -14
  77. package/dist/harness/scheduler.d.ts.map +1 -1
  78. package/dist/harness/scheduler.js +1459 -369
  79. package/dist/harness/scheduler.js.map +1 -1
  80. package/dist/harness/submissions.d.ts +23 -11
  81. package/dist/harness/submissions.d.ts.map +1 -1
  82. package/dist/harness/submissions.js +104 -36
  83. package/dist/harness/submissions.js.map +1 -1
  84. package/dist/harness/task-graph.d.ts +28 -8
  85. package/dist/harness/task-graph.d.ts.map +1 -1
  86. package/dist/harness/task-graph.js +85 -36
  87. package/dist/harness/task-graph.js.map +1 -1
  88. package/dist/harness/telemetry.d.ts +307 -0
  89. package/dist/harness/telemetry.d.ts.map +1 -0
  90. package/dist/harness/telemetry.js +136 -0
  91. package/dist/harness/telemetry.js.map +1 -0
  92. package/dist/harness/tool.d.ts +2 -0
  93. package/dist/harness/tool.d.ts.map +1 -1
  94. package/dist/harness/tool.js +223 -61
  95. package/dist/harness/tool.js.map +1 -1
  96. package/dist/harness/transcript.d.ts +72 -0
  97. package/dist/harness/transcript.d.ts.map +1 -0
  98. package/dist/harness/transcript.js +264 -0
  99. package/dist/harness/transcript.js.map +1 -0
  100. package/dist/harness/types.d.ts +302 -48
  101. package/dist/harness/types.d.ts.map +1 -1
  102. package/dist/harness/types.js.map +1 -1
  103. package/dist/harness/util.d.ts +7 -1
  104. package/dist/harness/util.d.ts.map +1 -1
  105. package/dist/harness/util.js +13 -0
  106. package/dist/harness/util.js.map +1 -1
  107. package/dist/harness/view.d.ts +43 -22
  108. package/dist/harness/view.d.ts.map +1 -1
  109. package/dist/harness/view.js +130 -71
  110. package/dist/harness/view.js.map +1 -1
  111. package/dist/index.d.ts +13 -9
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +7 -5
  114. package/dist/index.js.map +1 -1
  115. package/dist/search.d.ts +19 -3
  116. package/dist/search.d.ts.map +1 -1
  117. package/dist/search.js +107 -44
  118. package/dist/search.js.map +1 -1
  119. package/dist/session/cached-storage.d.ts +71 -0
  120. package/dist/session/cached-storage.d.ts.map +1 -0
  121. package/dist/session/cached-storage.js +269 -0
  122. package/dist/session/cached-storage.js.map +1 -0
  123. package/dist/session/conversation-tails.d.ts +33 -0
  124. package/dist/session/conversation-tails.d.ts.map +1 -0
  125. package/dist/session/conversation-tails.js +101 -0
  126. package/dist/session/conversation-tails.js.map +1 -0
  127. package/dist/session/keys.d.ts +68 -0
  128. package/dist/session/keys.d.ts.map +1 -0
  129. package/dist/session/keys.js +161 -0
  130. package/dist/session/keys.js.map +1 -0
  131. package/dist/session/session.d.ts +181 -29
  132. package/dist/session/session.d.ts.map +1 -1
  133. package/dist/session/session.js +1073 -183
  134. package/dist/session/session.js.map +1 -1
  135. package/dist/session/transaction.d.ts +86 -4
  136. package/dist/session/transaction.d.ts.map +1 -1
  137. package/dist/session/transaction.js +250 -38
  138. package/dist/session/transaction.js.map +1 -1
  139. package/dist/session/writer.d.ts +74 -0
  140. package/dist/session/writer.d.ts.map +1 -0
  141. package/dist/session/writer.js +294 -0
  142. package/dist/session/writer.js.map +1 -0
  143. package/dist/storage/indexeddb.d.ts +68 -0
  144. package/dist/storage/indexeddb.d.ts.map +1 -0
  145. package/dist/storage/indexeddb.js +881 -0
  146. package/dist/storage/indexeddb.js.map +1 -0
  147. package/dist/storage/jsonl/storage.d.ts +30 -2
  148. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  149. package/dist/storage/jsonl/storage.js +88 -24
  150. package/dist/storage/jsonl/storage.js.map +1 -1
  151. package/dist/storage/memory.d.ts +7 -2
  152. package/dist/storage/memory.d.ts.map +1 -1
  153. package/dist/storage/memory.js +29 -3
  154. package/dist/storage/memory.js.map +1 -1
  155. package/dist/storage/sqlite/cloudflare.d.ts +2 -51
  156. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
  157. package/dist/storage/sqlite/cloudflare.js +2 -133
  158. package/dist/storage/sqlite/cloudflare.js.map +1 -1
  159. package/dist/storage/sqlite/database.d.ts +1 -36
  160. package/dist/storage/sqlite/database.d.ts.map +1 -1
  161. package/dist/storage/sqlite/database.js.map +1 -1
  162. package/dist/storage/sqlite/index.d.ts +1 -1
  163. package/dist/storage/sqlite/index.d.ts.map +1 -1
  164. package/dist/storage/sqlite/index.js.map +1 -1
  165. package/dist/storage/sqlite/migrations.d.ts +2 -5
  166. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  167. package/dist/storage/sqlite/migrations.js +28 -28
  168. package/dist/storage/sqlite/migrations.js.map +1 -1
  169. package/dist/storage/sqlite/node.d.ts +2 -37
  170. package/dist/storage/sqlite/node.d.ts.map +1 -1
  171. package/dist/storage/sqlite/node.js +3 -174
  172. package/dist/storage/sqlite/node.js.map +1 -1
  173. package/dist/storage/sqlite/schema.d.ts +1 -3
  174. package/dist/storage/sqlite/schema.d.ts.map +1 -1
  175. package/dist/storage/sqlite/schema.js +2 -9
  176. package/dist/storage/sqlite/schema.js.map +1 -1
  177. package/dist/storage/sqlite/storage.d.ts +15 -3
  178. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  179. package/dist/storage/sqlite/storage.js +110 -36
  180. package/dist/storage/sqlite/storage.js.map +1 -1
  181. package/dist/storage/store/commit.d.ts +33 -0
  182. package/dist/storage/store/commit.d.ts.map +1 -0
  183. package/dist/storage/store/commit.js +286 -0
  184. package/dist/storage/store/commit.js.map +1 -0
  185. package/dist/storage/store/documents.d.ts +43 -0
  186. package/dist/storage/store/documents.d.ts.map +1 -0
  187. package/dist/storage/store/documents.js +102 -0
  188. package/dist/storage/store/documents.js.map +1 -0
  189. package/dist/storage/store/index.d.ts +2 -0
  190. package/dist/storage/store/index.d.ts.map +1 -0
  191. package/dist/storage/store/index.js +2 -0
  192. package/dist/storage/store/index.js.map +1 -0
  193. package/dist/storage/store/queries.d.ts +49 -0
  194. package/dist/storage/store/queries.d.ts.map +1 -0
  195. package/dist/storage/store/queries.js +93 -0
  196. package/dist/storage/store/queries.js.map +1 -0
  197. package/dist/storage/store/storage.d.ts +59 -0
  198. package/dist/storage/store/storage.d.ts.map +1 -0
  199. package/dist/storage/store/storage.js +438 -0
  200. package/dist/storage/store/storage.js.map +1 -0
  201. package/dist/storage/store/tables.d.ts +54 -0
  202. package/dist/storage/store/tables.d.ts.map +1 -0
  203. package/dist/storage/store/tables.js +108 -0
  204. package/dist/storage/store/tables.js.map +1 -0
  205. package/dist/testing/commit-writes.d.ts +5 -0
  206. package/dist/testing/commit-writes.d.ts.map +1 -0
  207. package/dist/testing/commit-writes.js +12 -0
  208. package/dist/testing/commit-writes.js.map +1 -0
  209. package/dist/testing/env-conformance.js +7 -7
  210. package/dist/testing/env-conformance.js.map +1 -1
  211. package/dist/testing/index.d.ts +1 -0
  212. package/dist/testing/index.d.ts.map +1 -1
  213. package/dist/testing/index.js +1 -0
  214. package/dist/testing/index.js.map +1 -1
  215. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  216. package/dist/testing/storage-benchmark.js +18 -17
  217. package/dist/testing/storage-benchmark.js.map +1 -1
  218. package/dist/testing/storage-conformance.d.ts.map +1 -1
  219. package/dist/testing/storage-conformance.js +369 -67
  220. package/dist/testing/storage-conformance.js.map +1 -1
  221. package/dist/timers.d.ts +21 -0
  222. package/dist/timers.d.ts.map +1 -0
  223. package/dist/timers.js +72 -0
  224. package/dist/timers.js.map +1 -0
  225. package/dist/tool-search/tool-search.d.ts +6 -12
  226. package/dist/tool-search/tool-search.d.ts.map +1 -1
  227. package/dist/tool-search/tool-search.js +7 -7
  228. package/dist/tool-search/tool-search.js.map +1 -1
  229. package/dist/tools/bash.d.ts +12 -12
  230. package/dist/tools/bash.d.ts.map +1 -1
  231. package/dist/tools/bash.js +16 -15
  232. package/dist/tools/bash.js.map +1 -1
  233. package/dist/tools/edit.d.ts +7 -7
  234. package/dist/tools/edit.d.ts.map +1 -1
  235. package/dist/tools/edit.js +9 -9
  236. package/dist/tools/edit.js.map +1 -1
  237. package/dist/tools/index.d.ts +32 -32
  238. package/dist/tools/read.d.ts +6 -6
  239. package/dist/tools/read.d.ts.map +1 -1
  240. package/dist/tools/read.js +19 -8
  241. package/dist/tools/read.js.map +1 -1
  242. package/dist/tools/write.d.ts +4 -4
  243. package/dist/tools/write.d.ts.map +1 -1
  244. package/dist/tools/write.js +5 -5
  245. package/dist/tools/write.js.map +1 -1
  246. package/dist/types.d.ts +331 -64
  247. package/dist/types.d.ts.map +1 -1
  248. package/dist/types.js.map +1 -1
  249. package/package.json +27 -10
  250. package/dist/harness/events.d.ts +0 -186
  251. package/dist/harness/events.d.ts.map +0 -1
  252. package/dist/harness/events.js +0 -373
  253. package/dist/harness/events.js.map +0 -1
  254. package/dist/session/observation.d.ts +0 -42
  255. package/dist/session/observation.d.ts.map +0 -1
  256. package/dist/session/observation.js +0 -281
  257. package/dist/session/observation.js.map +0 -1
@@ -45,6 +45,7 @@ export async function applyBoundary(tx, boundary, at, now) {
45
45
  const users = [...pick("steer", boundary.steeringMode), ...(final ? pick("followUp", boundary.followUpMode) : [])];
46
46
  users.sort((a, b) => a - b);
47
47
  // `appendEntry()` copies the drafts' values; the items are removed only afterwards.
48
+ let first;
48
49
  for (const index of writes) {
49
50
  const item = items[index];
50
51
  const draft = item.entry;
@@ -56,6 +57,7 @@ export async function applyBoundary(tx, boundary, at, now) {
56
57
  if (draft.head !== undefined)
57
58
  boundary.head = draft.head === "self" ? entry.id : draft.head;
58
59
  tx.placeSubmission(item.id, entry.id);
60
+ first ??= entry.id;
59
61
  }
60
62
  const placed = [];
61
63
  for (const index of users) {
@@ -63,11 +65,12 @@ export async function applyBoundary(tx, boundary, at, now) {
63
65
  const entry = await appendInput(tx, conversationId, item.content, item.entry, now);
64
66
  tx.placeSubmission(item.id, entry.id);
65
67
  placed.push(item.id);
68
+ first ??= entry.id;
66
69
  }
67
70
  const removed = [...writes, ...users].sort((a, b) => b - a);
68
71
  for (const index of removed)
69
72
  items.splice(index, 1);
70
- return { users: placed, reset };
73
+ return { users: placed, first, reset };
71
74
  }
72
75
  /** Append the entry an input is placed as: `entry`'s kind and data, or `pi.user`, with the user message of `content`. */
73
76
  export function appendInput(tx, conversationId, content, entry, now) {
@@ -105,15 +108,15 @@ export async function removeInboxItem(tx, conversationId, id) {
105
108
  }
106
109
  /**
107
110
  * Withdraw every queued input of a conversation, as `Conversation.abort()` and abort cascades do: each settles
108
- * `unanswered` with `aborted` and leaves the inbox; queued writes stay for later placement.
111
+ * `unanswered` with `reason` (default `aborted`) and leaves the inbox; queued writes stay for later placement.
109
112
  */
110
- export async function withdrawQueuedInputs(tx, conversationId) {
113
+ export async function withdrawQueuedInputs(tx, conversationId, reason = "aborted") {
111
114
  const items = (await tx.doc(InboxDoc, conversationId)).items;
112
115
  for (let index = items.length - 1; index >= 0; index--) {
113
116
  const item = items[index];
114
117
  if (item.mode === "write")
115
118
  continue;
116
- tx.settleSubmission(item.id, { status: "unanswered", reason: "aborted" });
119
+ tx.settleSubmission(item.id, { status: "unanswered", reason });
117
120
  items.splice(index, 1);
118
121
  }
119
122
  }
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/harness/inbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { copyJson, type Draft, type JsonRepresentation } from \"@earendil-works/chord\";\nimport { defineDoc } from \"../documents.ts\";\nimport { UserEntry } from \"../entries.ts\";\nimport type { ConversationId, EntryDraft, EntryId, EntryRecord, JsonObject, SubmissionId, Tx } from \"../types.ts\";\nimport type { InputEntry, QueueMode, Settings, UserInput } from \"./types.ts\";\n\n/** A queued submission: user input for a run, with the entry it is placed as when not `pi.user`, or an entry write. */\nexport type InboxItem =\n\t| {\n\t\t\tid: SubmissionId;\n\t\t\tmode: \"steer\" | \"followUp\";\n\t\t\tcontent: JsonRepresentation<UserInput>;\n\t\t\tentry?: JsonRepresentation<InputEntry>;\n\t }\n\t/** `entry` is an `EntryDraft`, stored as plain JSON. */\n\t| { id: SubmissionId; mode: \"write\"; entry: JsonObject };\n\n/** Built-in queue of one conversation's submissions waiting for a boundary, in ID order. */\nexport type InboxState = { items: InboxItem[] };\n\nexport const InboxDoc = defineDoc<InboxState>({\n\tkind: \"pi.inbox\",\n\tversion: 1,\n\tscope: \"conversation\",\n\thistory: \"latest\",\n\tfork: \"initial\",\n\tinitial: () => ({ items: [] }),\n\tcheckpointWhen: (value) => value.items.length === 0,\n});\n\n/** The settings a boundary reads, on the Session line. */\nexport type QueueModes = Pick<Settings, \"steeringMode\" | \"followUpMode\">;\n\n/** What a boundary reads before the commit's first table write, and the newest head it has seen so far. */\nexport type Boundary = {\n\treadonly conversationId: ConversationId;\n\treadonly inbox: Draft<InboxState>;\n\treadonly steeringMode: QueueMode;\n\treadonly followUpMode: QueueMode;\n\t/** Start of the active range, the newest head marker's `head`; advanced by heads written in this commit. */\n\thead: EntryId | undefined;\n};\n\n/** Selected user items, in ID order, and whether a `head: \"self\"` write (a reset) was placed. */\nexport type BoundaryResult = { readonly users: SubmissionId[]; readonly reset: boolean };\n\n/**\n * Read what a boundary needs. Table reads must precede the commit's first table write, so callers prepare the\n * boundary at the start of their commit.\n */\nexport async function prepareBoundary(tx: Tx, conversationId: ConversationId, modes: QueueModes): Promise<Boundary> {\n\tconst head = (await tx.latestHeadMarker(conversationId))?.head;\n\tconst inbox = await tx.doc(InboxDoc, conversationId);\n\treturn {\n\t\tconversationId,\n\t\tinbox,\n\t\tsteeringMode: modes.steeringMode,\n\t\tfollowUpMode: modes.followUpMode,\n\t\thead,\n\t};\n}\n\n/**\n * Place the queued items a boundary selects (spec §6): every write, the first or all steers, and at `final` the first\n * or all follow-ups. A selected reset turns a `postTools` boundary into `final`. Writes are placed first and user\n * items after them, each in ID order, so user items queued before a reset run in the new context. A write whose head\n * targets an entry before the active range, including a range started earlier in this commit, is stale. Selected and\n * stale items are removed positionally.\n */\nexport async function applyBoundary(\n\ttx: Tx,\n\tboundary: Boundary,\n\tat: \"postTools\" | \"final\",\n\tnow: number,\n): Promise<BoundaryResult> {\n\tconst { conversationId, inbox } = boundary;\n\tconst items = inbox.items;\n\tconst reset = hasQueuedReset(items);\n\tconst final = at === \"final\" || reset;\n\tconst pick = (mode: \"steer\" | \"followUp\", queueMode: QueueMode): number[] => {\n\t\tconst indexes = items.flatMap((item, index) => (item.mode === mode ? [index] : []));\n\t\treturn queueMode === \"all\" ? indexes : indexes.slice(0, 1);\n\t};\n\tconst writes = items.flatMap((item, index) => (item.mode === \"write\" ? [index] : []));\n\tconst users = [...pick(\"steer\", boundary.steeringMode), ...(final ? pick(\"followUp\", boundary.followUpMode) : [])];\n\tusers.sort((a, b) => a - b);\n\n\t// `appendEntry()` copies the drafts' values; the items are removed only afterwards.\n\tfor (const index of writes) {\n\t\tconst item = items[index] as Draft<Extract<InboxItem, { mode: \"write\" }>>;\n\t\tconst draft = item.entry as unknown as EntryDraft;\n\t\tif (isStale(boundary, draft)) {\n\t\t\ttx.settleSubmission(item.id, { status: \"unanswered\", reason: \"stale\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst entry = await tx.appendEntry(conversationId, draft);\n\t\tif (draft.head !== undefined) boundary.head = draft.head === \"self\" ? entry.id : draft.head;\n\t\ttx.placeSubmission(item.id, entry.id);\n\t}\n\tconst placed: SubmissionId[] = [];\n\tfor (const index of users) {\n\t\tconst item = items[index] as Draft<Extract<InboxItem, { mode: \"steer\" | \"followUp\" }>>;\n\t\tconst entry = await appendInput(tx, conversationId, item.content as UserInput, item.entry as InputEntry, now);\n\t\ttx.placeSubmission(item.id, entry.id);\n\t\tplaced.push(item.id);\n\t}\n\tconst removed = [...writes, ...users].sort((a, b) => b - a);\n\tfor (const index of removed) items.splice(index, 1);\n\treturn { users: placed, reset };\n}\n\n/** Append the entry an input is placed as: `entry`'s kind and data, or `pi.user`, with the user message of `content`. */\nexport function appendInput(\n\ttx: Tx,\n\tconversationId: ConversationId,\n\tcontent: UserInput,\n\tentry: InputEntry | undefined,\n\tnow: number,\n): Promise<EntryRecord> {\n\tconst message = { role: \"user\", content, timestamp: now } as const;\n\tif (entry === undefined) return tx.appendEntry(UserEntry, conversationId, { model: [message] });\n\treturn tx.appendEntry(conversationId, { ...pickInputEntry(entry), model: [message] });\n}\n\n/** The kind and data of an input's entry as strict JSON, without other fields an untyped caller might pass. */\nexport function pickInputEntry({ kind, data }: InputEntry): JsonRepresentation<InputEntry> {\n\tconst picked = data === undefined ? { kind } : { kind, data };\n\treturn copyJson(picked, { omitUndefinedProperties: true }) as JsonRepresentation<InputEntry>;\n}\n\n/**\n * Whether a write is display-only: it has no `model`, `head`, or `edits` (present counts, even if empty), so it changes\n * nothing the model sees and needs no boundary.\n */\nexport function isDisplayOnly(entry: EntryDraft): boolean {\n\treturn entry.model === undefined && entry.head === undefined && entry.edits === undefined;\n}\n\n/** Whether a reset (`head: \"self\"` write) is queued; entries appended before it leave the active range when it lands. */\nexport function hasQueuedReset(items: readonly InboxItem[]): boolean {\n\treturn items.some((item) => item.mode === \"write\" && item.entry.head === \"self\");\n}\n\n/** Whether a head write targets an entry before the active range, so placing it would bring back cut history. */\nexport function isStale(boundary: Boundary, entry: EntryDraft): boolean {\n\treturn typeof entry.head === \"number\" && boundary.head !== undefined && entry.head < boundary.head;\n}\n\n/** Remove a withdrawn submission's item; the caller settles the submission. */\nexport async function removeInboxItem(tx: Tx, conversationId: ConversationId, id: SubmissionId): Promise<void> {\n\tconst items = (await tx.doc(InboxDoc, conversationId)).items;\n\tconst index = items.findIndex((item) => item.id === id);\n\tif (index >= 0) items.splice(index, 1);\n}\n\n/**\n * Withdraw every queued input of a conversation, as `Conversation.abort()` and abort cascades do: each settles\n * `unanswered` with `aborted` and leaves the inbox; queued writes stay for later placement.\n */\nexport async function withdrawQueuedInputs(tx: Tx, conversationId: ConversationId): Promise<void> {\n\tconst items = (await tx.doc(InboxDoc, conversationId)).items;\n\tfor (let index = items.length - 1; index >= 0; index--) {\n\t\tconst item = items[index]!;\n\t\tif (item.mode === \"write\") continue;\n\t\ttx.settleSubmission(item.id, { status: \"unanswered\", reason: \"aborted\" });\n\t\titems.splice(index, 1);\n\t}\n}\n"]}
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/harness/inbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { copyJson, type Draft, type JsonRepresentation } from \"@earendil-works/chord\";\nimport { defineDoc } from \"../documents.ts\";\nimport { UserEntry } from \"../entries.ts\";\nimport type { ConversationId, EntryDraft, EntryId, EntryRecord, JsonObject, SubmissionId, Tx } from \"../types.ts\";\nimport type { InputEntry, QueueMode, Settings, UserInput } from \"./types.ts\";\n\n/** A queued submission: user input for a run, with the entry it is placed as when not `pi.user`, or an entry write. */\nexport type InboxItem =\n\t| {\n\t\t\tid: SubmissionId;\n\t\t\tmode: \"steer\" | \"followUp\";\n\t\t\tcontent: JsonRepresentation<UserInput>;\n\t\t\tentry?: JsonRepresentation<InputEntry>;\n\t }\n\t/** `entry` is an `EntryDraft`, stored as plain JSON. */\n\t| { id: SubmissionId; mode: \"write\"; entry: JsonObject };\n\n/** Built-in queue of one conversation's submissions waiting for a boundary, in ID order. */\nexport type InboxState = { items: InboxItem[] };\n\nexport const InboxDoc = defineDoc<InboxState>({\n\tkind: \"pi.inbox\",\n\tversion: 1,\n\tscope: \"conversation\",\n\thistory: \"latest\",\n\tfork: \"initial\",\n\tinitial: () => ({ items: [] }),\n\tcheckpointWhen: (value) => value.items.length === 0,\n});\n\n/** The settings a boundary reads, on the Session line. */\nexport type QueueModes = Pick<Settings, \"steeringMode\" | \"followUpMode\">;\n\n/** What a boundary reads before the commit's first table write, and the newest head it has seen so far. */\nexport type Boundary = {\n\treadonly conversationId: ConversationId;\n\treadonly inbox: Draft<InboxState>;\n\treadonly steeringMode: QueueMode;\n\treadonly followUpMode: QueueMode;\n\t/** Start of the active range, the newest head marker's `head`; advanced by heads written in this commit. */\n\thead: EntryId | undefined;\n};\n\n/**\n * Selected user items, in ID order, the first entry the boundary placed, a write's or a user item's (a run it starts\n * begins there, `LiveRun.first`), and whether a `head: \"self\"` write (a reset) was placed.\n */\nexport type BoundaryResult = {\n\treadonly users: SubmissionId[];\n\treadonly first: EntryId | undefined;\n\treadonly reset: boolean;\n};\n\n/**\n * Read what a boundary needs. Table reads must precede the commit's first table write, so callers prepare the\n * boundary at the start of their commit.\n */\nexport async function prepareBoundary(tx: Tx, conversationId: ConversationId, modes: QueueModes): Promise<Boundary> {\n\tconst head = (await tx.latestHeadMarker(conversationId))?.head;\n\tconst inbox = await tx.doc(InboxDoc, conversationId);\n\treturn {\n\t\tconversationId,\n\t\tinbox,\n\t\tsteeringMode: modes.steeringMode,\n\t\tfollowUpMode: modes.followUpMode,\n\t\thead,\n\t};\n}\n\n/**\n * Place the queued items a boundary selects (spec §6): every write, the first or all steers, and at `final` the first\n * or all follow-ups. A selected reset turns a `postTools` boundary into `final`. Writes are placed first and user\n * items after them, each in ID order, so user items queued before a reset run in the new context. A write whose head\n * targets an entry before the active range, including a range started earlier in this commit, is stale. Selected and\n * stale items are removed positionally.\n */\nexport async function applyBoundary(\n\ttx: Tx,\n\tboundary: Boundary,\n\tat: \"postTools\" | \"final\",\n\tnow: number,\n): Promise<BoundaryResult> {\n\tconst { conversationId, inbox } = boundary;\n\tconst items = inbox.items;\n\tconst reset = hasQueuedReset(items);\n\tconst final = at === \"final\" || reset;\n\tconst pick = (mode: \"steer\" | \"followUp\", queueMode: QueueMode): number[] => {\n\t\tconst indexes = items.flatMap((item, index) => (item.mode === mode ? [index] : []));\n\t\treturn queueMode === \"all\" ? indexes : indexes.slice(0, 1);\n\t};\n\tconst writes = items.flatMap((item, index) => (item.mode === \"write\" ? [index] : []));\n\tconst users = [...pick(\"steer\", boundary.steeringMode), ...(final ? pick(\"followUp\", boundary.followUpMode) : [])];\n\tusers.sort((a, b) => a - b);\n\n\t// `appendEntry()` copies the drafts' values; the items are removed only afterwards.\n\tlet first: EntryId | undefined;\n\tfor (const index of writes) {\n\t\tconst item = items[index] as Draft<Extract<InboxItem, { mode: \"write\" }>>;\n\t\tconst draft = item.entry as unknown as EntryDraft;\n\t\tif (isStale(boundary, draft)) {\n\t\t\ttx.settleSubmission(item.id, { status: \"unanswered\", reason: \"stale\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst entry = await tx.appendEntry(conversationId, draft);\n\t\tif (draft.head !== undefined) boundary.head = draft.head === \"self\" ? entry.id : draft.head;\n\t\ttx.placeSubmission(item.id, entry.id);\n\t\tfirst ??= entry.id;\n\t}\n\tconst placed: SubmissionId[] = [];\n\tfor (const index of users) {\n\t\tconst item = items[index] as Draft<Extract<InboxItem, { mode: \"steer\" | \"followUp\" }>>;\n\t\tconst entry = await appendInput(tx, conversationId, item.content as UserInput, item.entry as InputEntry, now);\n\t\ttx.placeSubmission(item.id, entry.id);\n\t\tplaced.push(item.id);\n\t\tfirst ??= entry.id;\n\t}\n\tconst removed = [...writes, ...users].sort((a, b) => b - a);\n\tfor (const index of removed) items.splice(index, 1);\n\treturn { users: placed, first, reset };\n}\n\n/** Append the entry an input is placed as: `entry`'s kind and data, or `pi.user`, with the user message of `content`. */\nexport function appendInput(\n\ttx: Tx,\n\tconversationId: ConversationId,\n\tcontent: UserInput,\n\tentry: InputEntry | undefined,\n\tnow: number,\n): Promise<EntryRecord> {\n\tconst message = { role: \"user\", content, timestamp: now } as const;\n\tif (entry === undefined) return tx.appendEntry(UserEntry, conversationId, { model: [message] });\n\treturn tx.appendEntry(conversationId, { ...pickInputEntry(entry), model: [message] });\n}\n\n/** The kind and data of an input's entry as strict JSON, without other fields an untyped caller might pass. */\nexport function pickInputEntry({ kind, data }: InputEntry): JsonRepresentation<InputEntry> {\n\tconst picked = data === undefined ? { kind } : { kind, data };\n\treturn copyJson(picked, { omitUndefinedProperties: true }) as JsonRepresentation<InputEntry>;\n}\n\n/**\n * Whether a write is display-only: it has no `model`, `head`, or `edits` (present counts, even if empty), so it changes\n * nothing the model sees and needs no boundary.\n */\nexport function isDisplayOnly(entry: EntryDraft): boolean {\n\treturn entry.model === undefined && entry.head === undefined && entry.edits === undefined;\n}\n\n/** Whether a reset (`head: \"self\"` write) is queued; entries appended before it leave the active range when it lands. */\nexport function hasQueuedReset(items: readonly InboxItem[]): boolean {\n\treturn items.some((item) => item.mode === \"write\" && item.entry.head === \"self\");\n}\n\n/** Whether a head write targets an entry before the active range, so placing it would bring back cut history. */\nexport function isStale(boundary: Boundary, entry: EntryDraft): boolean {\n\treturn typeof entry.head === \"number\" && boundary.head !== undefined && entry.head < boundary.head;\n}\n\n/** Remove a withdrawn submission's item; the caller settles the submission. */\nexport async function removeInboxItem(tx: Tx, conversationId: ConversationId, id: SubmissionId): Promise<void> {\n\tconst items = (await tx.doc(InboxDoc, conversationId)).items;\n\tconst index = items.findIndex((item) => item.id === id);\n\tif (index >= 0) items.splice(index, 1);\n}\n\n/**\n * Withdraw every queued input of a conversation, as `Conversation.abort()` and abort cascades do: each settles\n * `unanswered` with `reason` (default `aborted`) and leaves the inbox; queued writes stay for later placement.\n */\nexport async function withdrawQueuedInputs(tx: Tx, conversationId: ConversationId, reason = \"aborted\"): Promise<void> {\n\tconst items = (await tx.doc(InboxDoc, conversationId)).items;\n\tfor (let index = items.length - 1; index >= 0; index--) {\n\t\tconst item = items[index]!;\n\t\tif (item.mode === \"write\") continue;\n\t\ttx.settleSubmission(item.id, { status: \"unanswered\", reason });\n\t\titems.splice(index, 1);\n\t}\n}\n"]}
@@ -1,10 +1,26 @@
1
1
  import type { JsonValue } from "@earendil-works/chord";
2
- type JsonContainer = Record<string, JsonValue> | JsonValue[];
2
+ /** How `assignJson()` writes one value instead of assigning it leaf by leaf. */
3
+ export type AssignPlan =
4
+ /** The string grew by `text` since it was last assigned: append it, when the lengths agree. */
5
+ {
6
+ readonly kind: "append";
7
+ readonly text: string;
8
+ }
9
+ /** Leave the value as it is; the caller writes it. */
10
+ | {
11
+ readonly kind: "keep";
12
+ }
13
+ /** Plans for some members or items of a container. */
14
+ | {
15
+ readonly kind: "fields";
16
+ readonly fields: ReadonlyMap<string | number, AssignPlan>;
17
+ };
3
18
  /**
4
19
  * Assign `value` at `target[key]` leaf by leaf. Chord records a container assignment as one full set and only emits an
5
20
  * append when a string leaf is reassigned with a longer string, so writing the partial whole would store and publish the
6
- * complete message on every flush.
21
+ * complete message on every flush. That append costs a compare of the whole string, though; where `plan` says what a
22
+ * string member grew by, and the committed string plus that text has the new string's length, the text is appended
23
+ * with `appendText()` instead.
7
24
  */
8
- export declare function assignJson(target: JsonContainer, key: string | number, value: JsonValue): void;
9
- export {};
25
+ export declare function assignJson<T extends object, K extends keyof T & (string | number)>(target: T, key: K, value: T[K] & JsonValue, plan?: AssignPlan): void;
10
26
  //# sourceMappingURL=json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/harness/json.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,SAAS,EAAE,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAiB9F"}
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/harness/json.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,gFAAgF;AAChF,MAAM,MAAM,UAAU;AACrB,+FAA+F;AAC7F;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACpD,sDAAsD;GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC3B,sDAAsD;GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,CAAC,CAAA;CAAE,CAAC;AAE1F;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EACjF,MAAM,EAAE,CAAC,EACT,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,EACvB,IAAI,CAAC,EAAE,UAAU,GACf,IAAI,CAiCN"}
@@ -1,29 +1,47 @@
1
+ import { appendText } from "@earendil-works/chord/delta";
1
2
  /**
2
3
  * Assign `value` at `target[key]` leaf by leaf. Chord records a container assignment as one full set and only emits an
3
4
  * append when a string leaf is reassigned with a longer string, so writing the partial whole would store and publish the
4
- * complete message on every flush.
5
+ * complete message on every flush. That append costs a compare of the whole string, though; where `plan` says what a
6
+ * string member grew by, and the committed string plus that text has the new string's length, the text is appended
7
+ * with `appendText()` instead.
5
8
  */
6
- export function assignJson(target, key, value) {
9
+ export function assignJson(target, key, value, plan) {
10
+ if (plan?.kind === "keep")
11
+ return;
12
+ // Every target is a document container: JSON members or items, whatever its declared type names them.
7
13
  const slots = target;
8
- const current = slots[key];
14
+ // A `__proto__` member the target lacks reads through to Object.prototype; merging into that would pollute every
15
+ // object in the process. Other inherited members are functions, which are not merged into.
16
+ const current = key === "__proto__" && !Object.hasOwn(slots, key) ? undefined : slots[key];
17
+ const fields = plan?.kind === "fields" ? plan.fields : undefined;
9
18
  if (isRecord(current) && isRecord(value)) {
10
19
  for (const name of Object.keys(current))
11
20
  if (!Object.hasOwn(value, name))
12
21
  delete current[name];
13
22
  for (const [name, child] of Object.entries(value))
14
- assignJson(current, name, child);
23
+ assignJson(current, name, child, fields?.get(name));
15
24
  return;
16
25
  }
17
26
  if (Array.isArray(current) && Array.isArray(value) && current.length <= value.length) {
18
27
  const items = current;
19
28
  for (let index = 0; index < value.length; index++) {
20
29
  if (index < items.length)
21
- assignJson(items, index, value[index]);
30
+ assignJson(items, index, value[index], fields?.get(index));
22
31
  else
23
32
  items.push(value[index]);
24
33
  }
25
34
  return;
26
35
  }
36
+ if (plan?.kind === "append" &&
37
+ typeof current === "string" &&
38
+ typeof value === "string" &&
39
+ !Array.isArray(target) &&
40
+ current.length + plan.text.length === value.length) {
41
+ // The cast names what the checks above established: `target[key]` is a string.
42
+ appendText(target, String(key), plan.text);
43
+ return;
44
+ }
27
45
  if (current !== value)
28
46
  slots[key] = value;
29
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/harness/json.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAAqB,EAAE,GAAoB,EAAE,KAAgB;IACvF,MAAM,KAAK,GAAG,MAA4C,CAAC;IAC3D,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC;gBAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/F,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACpF,OAAO;IACR,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACtF,MAAM,KAAK,GAAG,OAAsB,CAAC;QACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YACnD,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM;gBAAE,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAE,CAAC,CAAC;;gBAC7D,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAE,CAAC,CAAC;QAChC,CAAC;QACD,OAAO;IACR,CAAC;IACD,IAAI,OAAO,KAAK,KAAK;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAC3C,CAAC;AAED,SAAS,QAAQ,CAAC,KAA4B;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["import type { JsonValue } from \"@earendil-works/chord\";\n\ntype JsonContainer = Record<string, JsonValue> | JsonValue[];\n\n/**\n * Assign `value` at `target[key]` leaf by leaf. Chord records a container assignment as one full set and only emits an\n * append when a string leaf is reassigned with a longer string, so writing the partial whole would store and publish the\n * complete message on every flush.\n */\nexport function assignJson(target: JsonContainer, key: string | number, value: JsonValue): void {\n\tconst slots = target as Record<string | number, JsonValue>;\n\tconst current = slots[key];\n\tif (isRecord(current) && isRecord(value)) {\n\t\tfor (const name of Object.keys(current)) if (!Object.hasOwn(value, name)) delete current[name];\n\t\tfor (const [name, child] of Object.entries(value)) assignJson(current, name, child);\n\t\treturn;\n\t}\n\tif (Array.isArray(current) && Array.isArray(value) && current.length <= value.length) {\n\t\tconst items = current as JsonValue[];\n\t\tfor (let index = 0; index < value.length; index++) {\n\t\t\tif (index < items.length) assignJson(items, index, value[index]!);\n\t\t\telse items.push(value[index]!);\n\t\t}\n\t\treturn;\n\t}\n\tif (current !== value) slots[key] = value;\n}\n\nfunction isRecord(value: JsonValue | undefined): value is Record<string, JsonValue> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"]}
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/harness/json.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAc,MAAM,6BAA6B,CAAC;AAWrE;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACzB,MAAS,EACT,GAAM,EACN,KAAuB,EACvB,IAAiB;IAEjB,IAAI,IAAI,EAAE,IAAI,KAAK,MAAM;QAAE,OAAO;IAClC,sGAAsG;IACtG,MAAM,KAAK,GAAG,MAA4C,CAAC;IAC3D,iHAAiH;IACjH,2FAA2F;IAC3F,MAAM,OAAO,GAA0B,GAAG,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClH,MAAM,MAAM,GAAG,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC;gBAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/F,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACvG,OAAO;IACR,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACtF,MAAM,KAAK,GAAG,OAAsB,CAAC;QACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YACnD,IAAI,KAAK,GAAG,KAAK,CAAC,MAAM;gBAAE,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAE,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;;gBACjF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAE,CAAC,CAAC;QAChC,CAAC;QACD,OAAO;IACR,CAAC;IACD,IACC,IAAI,EAAE,IAAI,KAAK,QAAQ;QACvB,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,KAAK,KAAK,QAAQ;QACzB,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EACjD,CAAC;QACF,+EAA+E;QAC/E,UAAU,CAAC,MAAuC,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5E,OAAO;IACR,CAAC;IACD,IAAI,OAAO,KAAK,KAAK;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAC3C,CAAC;AAED,SAAS,QAAQ,CAAC,KAA4B;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7E,CAAC","sourcesContent":["import type { JsonValue } from \"@earendil-works/chord\";\nimport { appendText, type Draft } from \"@earendil-works/chord/delta\";\n\n/** How `assignJson()` writes one value instead of assigning it leaf by leaf. */\nexport type AssignPlan =\n\t/** The string grew by `text` since it was last assigned: append it, when the lengths agree. */\n\t| { readonly kind: \"append\"; readonly text: string }\n\t/** Leave the value as it is; the caller writes it. */\n\t| { readonly kind: \"keep\" }\n\t/** Plans for some members or items of a container. */\n\t| { readonly kind: \"fields\"; readonly fields: ReadonlyMap<string | number, AssignPlan> };\n\n/**\n * Assign `value` at `target[key]` leaf by leaf. Chord records a container assignment as one full set and only emits an\n * append when a string leaf is reassigned with a longer string, so writing the partial whole would store and publish the\n * complete message on every flush. That append costs a compare of the whole string, though; where `plan` says what a\n * string member grew by, and the committed string plus that text has the new string's length, the text is appended\n * with `appendText()` instead.\n */\nexport function assignJson<T extends object, K extends keyof T & (string | number)>(\n\ttarget: T,\n\tkey: K,\n\tvalue: T[K] & JsonValue,\n\tplan?: AssignPlan,\n): void {\n\tif (plan?.kind === \"keep\") return;\n\t// Every target is a document container: JSON members or items, whatever its declared type names them.\n\tconst slots = target as Record<string | number, JsonValue>;\n\t// A `__proto__` member the target lacks reads through to Object.prototype; merging into that would pollute every\n\t// object in the process. Other inherited members are functions, which are not merged into.\n\tconst current: JsonValue | undefined = key === \"__proto__\" && !Object.hasOwn(slots, key) ? undefined : slots[key];\n\tconst fields = plan?.kind === \"fields\" ? plan.fields : undefined;\n\tif (isRecord(current) && isRecord(value)) {\n\t\tfor (const name of Object.keys(current)) if (!Object.hasOwn(value, name)) delete current[name];\n\t\tfor (const [name, child] of Object.entries(value)) assignJson(current, name, child, fields?.get(name));\n\t\treturn;\n\t}\n\tif (Array.isArray(current) && Array.isArray(value) && current.length <= value.length) {\n\t\tconst items = current as JsonValue[];\n\t\tfor (let index = 0; index < value.length; index++) {\n\t\t\tif (index < items.length) assignJson(items, index, value[index]!, fields?.get(index));\n\t\t\telse items.push(value[index]!);\n\t\t}\n\t\treturn;\n\t}\n\tif (\n\t\tplan?.kind === \"append\" &&\n\t\ttypeof current === \"string\" &&\n\t\ttypeof value === \"string\" &&\n\t\t!Array.isArray(target) &&\n\t\tcurrent.length + plan.text.length === value.length\n\t) {\n\t\t// The cast names what the checks above established: `target[key]` is a string.\n\t\tappendText(target as Draft<Record<string, string>>, String(key), plan.text);\n\t\treturn;\n\t}\n\tif (current !== value) slots[key] = value;\n}\n\nfunction isRecord(value: JsonValue | undefined): value is Record<string, JsonValue> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import type { Draft, JsonRepresentation, JsonValue } from "@earendil-works/chord";
2
2
  import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
3
3
  import type { Transaction } from "../session/transaction.ts";
4
- import type { EntryId, JsonObject, SubmissionId, SubmissionSettlement, TaskId, TaskRecord, Tx } from "../types.ts";
4
+ import type { CommitOptions, EntryId, JsonObject, SubmissionId, SubmissionSettlement, TaskId, TaskRecord, Tx } from "../types.ts";
5
5
  import type { SchedulerOutcome } from "./scheduler.ts";
6
6
  import type { CompactionReason, CompactionResult, ToolDiagnostic } from "./types.ts";
7
7
  /** What a running tool call publishes, shared by model-issued and nested calls. */
@@ -20,17 +20,16 @@ type SlotProgress = {
20
20
  };
21
21
  /** Presentation of one tool call of the current round. */
22
22
  export type ToolSlot = SlotProgress & {
23
- /**
24
- * Absent for a call not started yet (sequential round) and for a call its request did not offer, which starts `done`
25
- * with the `entry` generation wrote.
26
- */
23
+ /** Absent for a call not started yet (sequential round). */
27
24
  taskId?: TaskId;
25
+ /** A call to a tool its request did not offer: its task settles `tool_unavailable` without running anything. */
26
+ unoffered?: true;
28
27
  /** Result entry once done; absent when the tool task faulted or was orphaned. */
29
28
  entry?: EntryId;
30
29
  };
31
30
  /**
32
31
  * Presentation of one nested call: a call a running tool made through `executeTool()`. `callId` is
33
- * `<parentCallId>/<key>`, the call's ID in tool events and unique because keys contain no `/`. The Harness relates
32
+ * `<parentCallId>/<key>`, the call's ID, unique because keys contain no `/`. The Harness relates
34
33
  * slots by task ID: `taskId` is the call's tool task, `parentTaskId` the calling one, model-issued or nested.
35
34
  */
36
35
  export type NestedToolSlot = SlotProgress & {
@@ -55,7 +54,8 @@ export type NestedToolSummary = {
55
54
  /** Presentation of one live compaction task (spec §8.7). */
56
55
  export type CompactionStatus = {
57
56
  taskId: TaskId<CompactionResult>;
58
- reason: CompactionReason;
57
+ /** What started it: `compact()`, a threshold, or a context overflow. */
58
+ trigger: CompactionReason;
59
59
  /** Whether a generation waits for it: a compaction the generation owns. */
60
60
  blocking: boolean;
61
61
  attempt: number;
@@ -68,17 +68,71 @@ export type CompactionStatus = {
68
68
  /** The newest compaction that failed since a compaction last produced a summary (spec §8.7). */
69
69
  export type CompactionFailure = {
70
70
  taskId: TaskId<CompactionResult>;
71
- reason: CompactionReason;
71
+ /** What started the compaction that failed. */
72
+ trigger: CompactionReason;
72
73
  /** The failed or faulted outcome's error message, or why the scheduler orphaned the task. */
73
74
  error: string;
74
75
  };
76
+ /**
77
+ * Why a run ended without an answer:
78
+ * - `model_error`: the model's response failed, after any retries, or the compaction a request needed failed;
79
+ * - `no_model`: no model is configured, or the configured one is not available;
80
+ * - `aborted`: the run was aborted (`Conversation.abort()`, `abortTask()`); `detail` says when its abort handler was
81
+ * cut off `settings.restartLimit` times and did not finish;
82
+ * - `reset`: a queued reset started a new context at the post-tools boundary, before an answer (a tool's handoff
83
+ * continues the run);
84
+ * - `faulted`: the run's task threw where it cannot recover, or storage rejected its commit;
85
+ * - `orphaned`: the run's task was aborted while no registered definition could take it;
86
+ * - `interrupted`: the host stopped while the model was answering, more often than `settings.retry` retries;
87
+ * - `restart_limit`: the run's generation was cut off `settings.restartLimit` times in a row; a request or poll it was
88
+ * cut off during counts as a failed attempt first, so this ends one whose charge itself keeps failing;
89
+ * - `nothing_to_answer`: the context did not end in a user message or tool results (empty, or ending in an answer), so
90
+ * no request was sent, as for a continue after an answer;
91
+ * - `turn_limit`: a tool round reached the conversation's `maxTurns`; a continue resumes on its results.
92
+ */
93
+ export type RunFailureReason = "model_error" | "no_model" | "aborted" | "reset" | "faulted" | "orphaned" | "interrupted" | "restart_limit" | "nothing_to_answer" | "turn_limit";
94
+ /** The newest run that ended without an answer (spec §8.2). */
95
+ export type RunFailure = {
96
+ /** The task that ended the run: the run's task at its end. */
97
+ taskId: TaskId;
98
+ /** The run's inputs, settled `unanswered`. */
99
+ inputs: SubmissionId[];
100
+ reason: RunFailureReason;
101
+ /**
102
+ * `model_error`: the error text; `no_model`: which model is missing; `faulted`: the error message; `orphaned`: why
103
+ * the task could not run (`missing_task`, `task_too_old`, `migration_failed`); `interrupted` and `restart_limit`: what
104
+ * was cut off, and how often; `turn_limit`: the tool rounds the run took, `"1 turn"` or `"N turns"`.
105
+ */
106
+ detail?: string;
107
+ /** The failed run's `run.first`, so a continue's `afterRun` hooks see its history too. */
108
+ first?: EntryId;
109
+ };
110
+ /** Run control (spec §8.2). */
111
+ export type LiveRun = {
112
+ /** The task that settles the run's inputs: its generation, handed from one to the next. */
113
+ taskId: TaskId;
114
+ /** The run's placed inputs, settled when it ends; `[]` once it answered and runs only its `afterRun` tasks. */
115
+ inputs: SubmissionId[];
116
+ /** Tool rounds so far, which `AgentState.maxTurns` bounds; an `onYield` continuation starts the count over. */
117
+ turns: number;
118
+ /**
119
+ * The run's first entry: the first one the boundary that started it placed, a continue's the failed run's it
120
+ * continues; otherwise its first tool-calling answer. A run started before it was recorded may lack it; `afterRun`
121
+ * hooks then get the answer as the first entry.
122
+ */
123
+ first?: EntryId;
124
+ /**
125
+ * Present once the run answered and its generation waits for the tasks `afterRun` hooks asked for, listed once
126
+ * created: queued items stay queued until they are terminal, and the run then ends without a failure.
127
+ */
128
+ afterRun?: {
129
+ tasks?: TaskId[];
130
+ };
131
+ };
75
132
  /** Built-in live conversation state: run control and presentation of the current generation and tool round. */
76
133
  export type LiveState = {
77
- /** Run control: the task that settles the run's inputs, and those inputs; present exactly while busy. */
78
- run?: {
79
- taskId: TaskId;
80
- inputs: SubmissionId[];
81
- };
134
+ /** Run control; present exactly while busy, also while a run that answered runs its `afterRun` tasks. */
135
+ run?: LiveRun;
82
136
  /** Presentation of the current generation attempt. */
83
137
  generation?: {
84
138
  attempt: number;
@@ -108,13 +162,65 @@ export type LiveState = {
108
162
  * removes it; one that is aborted or finds nothing to compact leaves it.
109
163
  */
110
164
  compactionFailure?: CompactionFailure;
165
+ /**
166
+ * Why the newest run ended without an answer, so a client can show it after the run ended. Set in the commit that
167
+ * ends the run; removed when a later commit starts a run. A run started in the same commit, by
168
+ * `afterFailedRun: "continue"` or after a reset, keeps it.
169
+ */
170
+ runFailure?: RunFailure;
111
171
  };
112
172
  export declare const LiveDoc: import("../types.ts").ConversationDocToken<LiveState>;
113
173
  /**
114
- * End the run owned by `taskId`: settle each of its inputs and remove `run`. Always removes `generation`, `tools`, and
115
- * `nestedTools`, whose presentation belongs to the ending run.
174
+ * How the progress throttles commit a partial answer and running tool output into `pi.live`: deferred, so storage gets
175
+ * them together, at the next commit that reads or writes `pi.live` or after `settings.progress.persistWithinMs`. Every
176
+ * commit that consumes or clears streamed fields reads `pi.live`, so it stores them with its own changes (spec §8.2).
177
+ */
178
+ export declare const PROGRESS_COMMIT: CommitOptions;
179
+ /**
180
+ * Built-in task kinds that end themselves once cut off `settings.restartLimit` times (`TaskRecord.restarts`); the
181
+ * scheduler faults a task of any other kind instead (spec §5.4).
182
+ */
183
+ export declare const LIMITS_RESTARTS: ReadonlySet<string>;
184
+ /** Whether `task` was cut off `settings.restartLimit` times in a row, so its phase ends it instead of running. */
185
+ export declare function cutOffTooOften(task: {
186
+ readonly restarts?: number;
187
+ }, runtime: {
188
+ readonly settings: {
189
+ readonly restartLimit: number;
190
+ };
191
+ }): boolean;
192
+ /**
193
+ * Whether a task at the limit may still do the cleanup it does when it ends, such as converting a partial: only at the
194
+ * limit. Past it, that cleanup may be what cut the task off, so the task ends without it.
195
+ */
196
+ export declare function salvageable(task: {
197
+ readonly restarts?: number;
198
+ }, runtime: {
199
+ readonly settings: {
200
+ readonly restartLimit: number;
201
+ };
202
+ }): boolean;
203
+ /** How often a task was cut off, for a message: "once", or "3 times in a row". */
204
+ export declare function cutOffCount(task: {
205
+ readonly restarts?: number;
206
+ }): string;
207
+ /** How a run ends: with an answer, or without one, settling its inputs `unanswered` and recording why. */
208
+ export type RunEnd = {
209
+ readonly status: "done";
210
+ readonly answer: EntryId;
211
+ } | {
212
+ readonly status: "unanswered";
213
+ readonly reason: RunFailureReason;
214
+ readonly detail?: string;
215
+ /** The inputs' settlement; default `unanswered` with `reason` and `detail`. */
216
+ readonly settlement?: SubmissionSettlement;
217
+ };
218
+ /**
219
+ * End the run owned by `taskId`: settle each of its inputs, remove `run`, and, without an answer, record
220
+ * `runFailure`. A run that answered already, and runs only its `afterRun` tasks, records no failure, however it ends.
221
+ * Always removes `generation`, `tools`, and `nestedTools`, whose presentation belongs to the ending run.
116
222
  */
117
- export declare function endRun(tx: Tx, live: Draft<LiveState>, taskId: TaskId, settlement: SubmissionSettlement): void;
223
+ export declare function endRun(tx: Tx, live: Draft<LiveState>, taskId: TaskId, end: RunEnd): void;
118
224
  /** Add the status of a compaction task created in this commit; statuses stay in task ID order. */
119
225
  export declare function addCompactionStatus(live: Draft<LiveState>, status: CompactionStatus): void;
120
226
  /** The status of compaction task `taskId`, if listed. */
@@ -1 +1 @@
1
- {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../src/harness/live.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAErE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEnH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAErF,mFAAmF;AACnF,KAAK,YAAY,GAAG;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,uDAAuD;IACvD,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG;IACrC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,SAAS,EAAE,UAAU,CAAC;IACtB,8GAA8G;IAC9G,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC5B,CAAC;AAEF,qHAAqH;AACrH,MAAM,MAAM,iBAAiB,GAAG;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACtC,CAAC;AAEF,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,6FAA6F;IAC7F,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+GAA+G;AAC/G,MAAM,MAAM,SAAS,GAAG;IACvB,yGAAyG;IACzG,GAAG,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;IACjD,sDAAsD;IACtD,UAAU,CAAC,EAAE;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,6DAA6D;QAC7D,OAAO,CAAC,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;QAC/C,+CAA+C;QAC/C,KAAK,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QACtC,oDAAoD;QACpD,QAAQ,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;KAC9B,CAAC;IACF,uHAAuH;IACvH,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,gEAAgE;IAChE,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACtC,CAAC;AAEF,eAAO,MAAM,OAAO,uDAgBlB,CAAC;AAOH;;;GAGG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,oBAAoB,GAAG,IAAI,CAQ7G;AAED,kGAAkG;AAClG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAG1F;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,gBAAgB,CAAC,GAAG,SAAS,CAE5G;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAMnF;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIhG;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,QAAQ,GAAG,cAAc,CAAC,GAAG,SAAS,CAc7G;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,GAAG,cAAc,CAAC,GAAG,IAAI,CAGvE;AAED,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAS9E;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,GAAG,IAAI,CAM7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAC3C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,UAAU,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,EACnD,OAAO,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../src/harness/live.ts"],"names":[],"mappings":"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"}
@@ -2,11 +2,24 @@ import { defineDoc } from "../documents.js";
2
2
  import { convertPartial } from "./generation.js";
3
3
  export const LiveDoc = defineDoc({
4
4
  kind: "pi.live",
5
- version: 1,
5
+ version: 3,
6
6
  scope: "conversation",
7
7
  history: "latest",
8
8
  fork: "initial",
9
9
  initial: () => ({}),
10
+ // Version 1 named a compaction's trigger `reason`, in `compactions` and `compactionFailure`. Versions 1 and 2 (as
11
+ // builds before `run.turns` stored it) may lack `run.turns`: a run in flight across the upgrade counts its turns
12
+ // from 0. Version 2 already names the trigger `trigger`, so renaming leaves it alone.
13
+ migrate: (value) => {
14
+ const live = value;
15
+ const { run, compactions, compactionFailure } = live;
16
+ return {
17
+ ...value,
18
+ ...(run === undefined ? {} : { run: { ...run, turns: typeof run.turns === "number" ? run.turns : 0 } }),
19
+ ...(compactions === undefined ? {} : { compactions: compactions.map(renameReason) }),
20
+ ...(compactionFailure === undefined ? {} : { compactionFailure: renameReason(compactionFailure) }),
21
+ };
22
+ },
10
23
  // REMINDER: a complete base whenever nothing runs (spec §8.2): no generation and no running tool slot. That holds
11
24
  // while idle, in the commit handing a generation over to its tool round, and between tools, so the delta chain
12
25
  // spans at most one generation or the overlapping execution of one round's tools. A slot holds output only while
@@ -16,18 +29,72 @@ export const LiveDoc = defineDoc({
16
29
  !(value.tools ?? []).some((slot) => slot.status === "running") &&
17
30
  !(value.nestedTools ?? []).some((slot) => slot.status === "running"),
18
31
  });
32
+ /** A copy of a version 1 compaction status or failure with `reason` renamed `trigger`, its keys in their order. */
33
+ function renameReason(value) {
34
+ return Object.fromEntries(Object.entries(value).map(([key, field]) => [key === "reason" ? "trigger" : key, field]));
35
+ }
36
+ /**
37
+ * How the progress throttles commit a partial answer and running tool output into `pi.live`: deferred, so storage gets
38
+ * them together, at the next commit that reads or writes `pi.live` or after `settings.progress.persistWithinMs`. Every
39
+ * commit that consumes or clears streamed fields reads `pi.live`, so it stores them with its own changes (spec §8.2).
40
+ */
41
+ export const PROGRESS_COMMIT = { persistence: { kind: "deferred" } };
19
42
  /** Built-in task kinds that can own `pi.live.run`. */
20
43
  const RUN_TASK_KINDS = new Set(["pi.generation"]);
21
44
  const TOOL_TASK_KIND = "pi.tool";
22
45
  const COMPACTION_TASK_KIND = "pi.compaction";
23
46
  /**
24
- * End the run owned by `taskId`: settle each of its inputs and remove `run`. Always removes `generation`, `tools`, and
25
- * `nestedTools`, whose presentation belongs to the ending run.
47
+ * Built-in task kinds that end themselves once cut off `settings.restartLimit` times (`TaskRecord.restarts`); the
48
+ * scheduler faults a task of any other kind instead (spec §5.4).
49
+ */
50
+ export const LIMITS_RESTARTS = new Set([...RUN_TASK_KINDS, TOOL_TASK_KIND, COMPACTION_TASK_KIND]);
51
+ /** Whether `task` was cut off `settings.restartLimit` times in a row, so its phase ends it instead of running. */
52
+ export function cutOffTooOften(task, runtime) {
53
+ // The settings are read only for a task that was cut off at all, so the normal path resolves nothing more.
54
+ return task.restarts !== undefined && task.restarts >= runtime.settings.restartLimit;
55
+ }
56
+ /**
57
+ * Whether a task at the limit may still do the cleanup it does when it ends, such as converting a partial: only at the
58
+ * limit. Past it, that cleanup may be what cut the task off, so the task ends without it.
26
59
  */
27
- export function endRun(tx, live, taskId, settlement) {
60
+ export function salvageable(task, runtime) {
61
+ return (task.restarts ?? 0) <= runtime.settings.restartLimit;
62
+ }
63
+ /** How often a task was cut off, for a message: "once", or "3 times in a row". */
64
+ export function cutOffCount(task) {
65
+ const restarts = task.restarts ?? 0;
66
+ return restarts === 1 ? "once" : `${restarts} times in a row`;
67
+ }
68
+ /**
69
+ * End the run owned by `taskId`: settle each of its inputs, remove `run`, and, without an answer, record
70
+ * `runFailure`. A run that answered already, and runs only its `afterRun` tasks, records no failure, however it ends.
71
+ * Always removes `generation`, `tools`, and `nestedTools`, whose presentation belongs to the ending run.
72
+ */
73
+ export function endRun(tx, live, taskId, end) {
28
74
  if (live.run?.taskId === taskId) {
29
- for (const id of live.run.inputs)
30
- tx.settleSubmission(id, settlement);
75
+ const inputs = [...live.run.inputs];
76
+ const { first } = live.run;
77
+ if (end.status === "done") {
78
+ for (const id of inputs)
79
+ tx.settleSubmission(id, { status: "done", answer: end.answer });
80
+ }
81
+ else if (live.run.afterRun === undefined) {
82
+ const { reason, detail } = end;
83
+ const settlement = end.settlement ?? {
84
+ status: "unanswered",
85
+ reason,
86
+ ...(detail === undefined ? {} : { detail }),
87
+ };
88
+ for (const id of inputs)
89
+ tx.settleSubmission(id, settlement);
90
+ live.runFailure = {
91
+ taskId,
92
+ inputs,
93
+ reason,
94
+ ...(detail === undefined ? {} : { detail }),
95
+ ...(first === undefined ? {} : { first }),
96
+ };
97
+ }
31
98
  delete live.run;
32
99
  }
33
100
  delete live.generation;
@@ -58,7 +125,7 @@ export function removeCompactionStatus(live, taskId) {
58
125
  export function failCompactionStatus(live, taskId, error) {
59
126
  const status = compactionStatus(live, taskId);
60
127
  if (status !== undefined)
61
- live.compactionFailure = { taskId: status.taskId, reason: status.reason, error };
128
+ live.compactionFailure = { taskId: status.taskId, trigger: status.trigger, error };
62
129
  removeCompactionStatus(live, taskId);
63
130
  }
64
131
  /**
@@ -139,9 +206,16 @@ export async function settleSchedulerOutcome(tx, record, outcome) {
139
206
  removeNestedSlots(live, record.id);
140
207
  return;
141
208
  }
209
+ // Only an abort handler is faulted for the limit here (the built-in phases end themselves): the task was aborted,
210
+ // and keeps that meaning. Its run ends `aborted`, with the limit as detail, and an aborted compaction is no failure.
211
+ const detail = outcome.status === "faulted" ? outcome.error.detail : undefined;
212
+ const cutOff = typeof detail === "object" && detail !== null && "reason" in detail && detail.reason === "restart_limit";
142
213
  if (record.kind === COMPACTION_TASK_KIND) {
143
- const error = outcome.status === "faulted" ? outcome.error.message : outcome.reason;
144
- failCompactionStatus(await tx.doc(LiveDoc, record.conversationId), record.id, error);
214
+ const live = await tx.doc(LiveDoc, record.conversationId);
215
+ if (cutOff)
216
+ removeCompactionStatus(live, record.id);
217
+ else
218
+ failCompactionStatus(live, record.id, outcome.status === "faulted" ? outcome.error.message : outcome.reason);
145
219
  return;
146
220
  }
147
221
  if (!RUN_TASK_KINDS.has(record.kind))
@@ -149,9 +223,16 @@ export async function settleSchedulerOutcome(tx, record, outcome) {
149
223
  const live = await tx.doc(LiveDoc, record.conversationId);
150
224
  if (live.run?.taskId !== record.id)
151
225
  return;
152
- await convertPartial(tx, live, record.conversationId);
226
+ // Cut off, the abort handler may have died converting the partial: the run ends without it.
227
+ if (!cutOff)
228
+ await convertPartial(tx, live, record.conversationId);
153
229
  endRun(tx, live, record.id, outcome.status === "faulted"
154
- ? { status: "unanswered", reason: "faulted", detail: outcome.error.message }
155
- : { status: "unanswered", reason: outcome.reason });
230
+ ? { status: "unanswered", reason: cutOff ? "aborted" : "faulted", detail: outcome.error.message }
231
+ : {
232
+ status: "unanswered",
233
+ reason: "orphaned",
234
+ detail: outcome.reason,
235
+ settlement: { status: "unanswered", reason: outcome.reason },
236
+ });
156
237
  }
157
238
  //# sourceMappingURL=live.js.map