@shipfox/api-logs 2.0.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 (342) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +179 -0
  5. package/LICENSE +21 -0
  6. package/README.md +151 -0
  7. package/dist/api/object-storage.d.ts +69 -0
  8. package/dist/api/object-storage.d.ts.map +1 -0
  9. package/dist/api/object-storage.js +199 -0
  10. package/dist/api/object-storage.js.map +1 -0
  11. package/dist/config.d.ts +20 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +108 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/append-logs.d.ts +28 -0
  16. package/dist/core/append-logs.d.ts.map +1 -0
  17. package/dist/core/append-logs.js +318 -0
  18. package/dist/core/append-logs.js.map +1 -0
  19. package/dist/core/budget.d.ts +12 -0
  20. package/dist/core/budget.d.ts.map +1 -0
  21. package/dist/core/budget.js +9 -0
  22. package/dist/core/budget.js.map +1 -0
  23. package/dist/core/close-stream.d.ts +27 -0
  24. package/dist/core/close-stream.d.ts.map +1 -0
  25. package/dist/core/close-stream.js +59 -0
  26. package/dist/core/close-stream.js.map +1 -0
  27. package/dist/core/compaction.d.ts +28 -0
  28. package/dist/core/compaction.d.ts.map +1 -0
  29. package/dist/core/compaction.js +49 -0
  30. package/dist/core/compaction.js.map +1 -0
  31. package/dist/core/entities/attempt-stream.d.ts +37 -0
  32. package/dist/core/entities/attempt-stream.d.ts.map +1 -0
  33. package/dist/core/entities/attempt-stream.js +15 -0
  34. package/dist/core/entities/attempt-stream.js.map +1 -0
  35. package/dist/core/entities/job-accounting.d.ts +12 -0
  36. package/dist/core/entities/job-accounting.d.ts.map +1 -0
  37. package/dist/core/entities/job-accounting.js +6 -0
  38. package/dist/core/entities/job-accounting.js.map +1 -0
  39. package/dist/core/entities/log-object.d.ts +13 -0
  40. package/dist/core/entities/log-object.d.ts.map +1 -0
  41. package/dist/core/entities/log-object.js +9 -0
  42. package/dist/core/entities/log-object.js.map +1 -0
  43. package/dist/core/errors.d.ts +26 -0
  44. package/dist/core/errors.d.ts.map +1 -0
  45. package/dist/core/errors.js +31 -0
  46. package/dist/core/errors.js.map +1 -0
  47. package/dist/core/finalize-attempt-stream.d.ts +22 -0
  48. package/dist/core/finalize-attempt-stream.d.ts.map +1 -0
  49. package/dist/core/finalize-attempt-stream.js +48 -0
  50. package/dist/core/finalize-attempt-stream.js.map +1 -0
  51. package/dist/core/read-logs.d.ts +33 -0
  52. package/dist/core/read-logs.d.ts.map +1 -0
  53. package/dist/core/read-logs.js +49 -0
  54. package/dist/core/read-logs.js.map +1 -0
  55. package/dist/core/reap-stale-open-streams.d.ts +27 -0
  56. package/dist/core/reap-stale-open-streams.d.ts.map +1 -0
  57. package/dist/core/reap-stale-open-streams.js +50 -0
  58. package/dist/core/reap-stale-open-streams.js.map +1 -0
  59. package/dist/core/retention.d.ts +32 -0
  60. package/dist/core/retention.d.ts.map +1 -0
  61. package/dist/core/retention.js +148 -0
  62. package/dist/core/retention.js.map +1 -0
  63. package/dist/core/session/claude/rows.d.ts +6 -0
  64. package/dist/core/session/claude/rows.d.ts.map +1 -0
  65. package/dist/core/session/claude/rows.js +145 -0
  66. package/dist/core/session/claude/rows.js.map +1 -0
  67. package/dist/core/session/claude-parser.d.ts +4 -0
  68. package/dist/core/session/claude-parser.d.ts.map +1 -0
  69. package/dist/core/session/claude-parser.js +42 -0
  70. package/dist/core/session/claude-parser.js.map +1 -0
  71. package/dist/core/session/entry-schema.d.ts +51 -0
  72. package/dist/core/session/entry-schema.d.ts.map +1 -0
  73. package/dist/core/session/entry-schema.js +61 -0
  74. package/dist/core/session/entry-schema.js.map +1 -0
  75. package/dist/core/session/object.d.ts +12 -0
  76. package/dist/core/session/object.d.ts.map +1 -0
  77. package/dist/core/session/object.js +50 -0
  78. package/dist/core/session/object.js.map +1 -0
  79. package/dist/core/session/parse-session.d.ts +5 -0
  80. package/dist/core/session/parse-session.d.ts.map +1 -0
  81. package/dist/core/session/parse-session.js +18 -0
  82. package/dist/core/session/parse-session.js.map +1 -0
  83. package/dist/core/session/pi/message-rows.d.ts +4 -0
  84. package/dist/core/session/pi/message-rows.d.ts.map +1 -0
  85. package/dist/core/session/pi/message-rows.js +113 -0
  86. package/dist/core/session/pi/message-rows.js.map +1 -0
  87. package/dist/core/session/pi/metadata.d.ts +18 -0
  88. package/dist/core/session/pi/metadata.d.ts.map +1 -0
  89. package/dist/core/session/pi/metadata.js +120 -0
  90. package/dist/core/session/pi/metadata.js.map +1 -0
  91. package/dist/core/session/pi/text.d.ts +12 -0
  92. package/dist/core/session/pi/text.d.ts.map +1 -0
  93. package/dist/core/session/pi/text.js +59 -0
  94. package/dist/core/session/pi/text.js.map +1 -0
  95. package/dist/core/session/pi-parser.d.ts +5 -0
  96. package/dist/core/session/pi-parser.d.ts.map +1 -0
  97. package/dist/core/session/pi-parser.js +73 -0
  98. package/dist/core/session/pi-parser.js.map +1 -0
  99. package/dist/core/session/rows.d.ts +7 -0
  100. package/dist/core/session/rows.d.ts.map +1 -0
  101. package/dist/core/session/rows.js +39 -0
  102. package/dist/core/session/rows.js.map +1 -0
  103. package/dist/core/session/session-record.d.ts +5 -0
  104. package/dist/core/session/session-record.d.ts.map +1 -0
  105. package/dist/core/session/session-record.js +3 -0
  106. package/dist/core/session/session-record.js.map +1 -0
  107. package/dist/db/accounting.d.ts +33 -0
  108. package/dist/db/accounting.d.ts.map +1 -0
  109. package/dist/db/accounting.js +52 -0
  110. package/dist/db/accounting.js.map +1 -0
  111. package/dist/db/chunks.d.ts +62 -0
  112. package/dist/db/chunks.d.ts.map +1 -0
  113. package/dist/db/chunks.js +88 -0
  114. package/dist/db/chunks.js.map +1 -0
  115. package/dist/db/db.d.ts +752 -0
  116. package/dist/db/db.d.ts.map +1 -0
  117. package/dist/db/db.js +24 -0
  118. package/dist/db/db.js.map +1 -0
  119. package/dist/db/index.d.ts +2 -0
  120. package/dist/db/index.d.ts.map +1 -0
  121. package/dist/db/index.js +5 -0
  122. package/dist/db/index.js.map +1 -0
  123. package/dist/db/schema/attempt-streams.d.ts +304 -0
  124. package/dist/db/schema/attempt-streams.d.ts.map +1 -0
  125. package/dist/db/schema/attempt-streams.js +100 -0
  126. package/dist/db/schema/attempt-streams.js.map +1 -0
  127. package/dist/db/schema/chunks.d.ts +166 -0
  128. package/dist/db/schema/chunks.d.ts.map +1 -0
  129. package/dist/db/schema/chunks.js +47 -0
  130. package/dist/db/schema/chunks.js.map +1 -0
  131. package/dist/db/schema/common.d.ts +29 -0
  132. package/dist/db/schema/common.d.ts.map +1 -0
  133. package/dist/db/schema/common.js +11 -0
  134. package/dist/db/schema/common.js.map +1 -0
  135. package/dist/db/schema/job-accounting.d.ts +143 -0
  136. package/dist/db/schema/job-accounting.d.ts.map +1 -0
  137. package/dist/db/schema/job-accounting.js +43 -0
  138. package/dist/db/schema/job-accounting.js.map +1 -0
  139. package/dist/db/schema/outbox.d.ts +195 -0
  140. package/dist/db/schema/outbox.d.ts.map +1 -0
  141. package/dist/db/schema/outbox.js +5 -0
  142. package/dist/db/schema/outbox.js.map +1 -0
  143. package/dist/db/streams.d.ts +141 -0
  144. package/dist/db/streams.d.ts.map +1 -0
  145. package/dist/db/streams.js +197 -0
  146. package/dist/db/streams.js.map +1 -0
  147. package/dist/index.d.ts +4 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +81 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/metrics/index.d.ts +2 -0
  152. package/dist/metrics/index.d.ts.map +1 -0
  153. package/dist/metrics/index.js +3 -0
  154. package/dist/metrics/index.js.map +1 -0
  155. package/dist/metrics/instance.d.ts +14 -0
  156. package/dist/metrics/instance.d.ts.map +1 -0
  157. package/dist/metrics/instance.js +16 -0
  158. package/dist/metrics/instance.js.map +1 -0
  159. package/dist/metrics/service.d.ts +2 -0
  160. package/dist/metrics/service.d.ts.map +1 -0
  161. package/dist/metrics/service.js +20 -0
  162. package/dist/metrics/service.js.map +1 -0
  163. package/dist/presentation/dto/read-logs.d.ts +4 -0
  164. package/dist/presentation/dto/read-logs.d.ts.map +1 -0
  165. package/dist/presentation/dto/read-logs.js +22 -0
  166. package/dist/presentation/dto/read-logs.js.map +1 -0
  167. package/dist/presentation/routes/append-logs.d.ts +2 -0
  168. package/dist/presentation/routes/append-logs.d.ts.map +1 -0
  169. package/dist/presentation/routes/append-logs.js +71 -0
  170. package/dist/presentation/routes/append-logs.js.map +1 -0
  171. package/dist/presentation/routes/index.d.ts +3 -0
  172. package/dist/presentation/routes/index.d.ts.map +1 -0
  173. package/dist/presentation/routes/index.js +32 -0
  174. package/dist/presentation/routes/index.js.map +1 -0
  175. package/dist/presentation/routes/read-logs.d.ts +2 -0
  176. package/dist/presentation/routes/read-logs.d.ts.map +1 -0
  177. package/dist/presentation/routes/read-logs.js +41 -0
  178. package/dist/presentation/routes/read-logs.js.map +1 -0
  179. package/dist/presentation/subscribers/on-job-terminated.d.ts +8 -0
  180. package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
  181. package/dist/presentation/subscribers/on-job-terminated.js +32 -0
  182. package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
  183. package/dist/presentation/subscribers/on-log-stream-closed.d.ts +9 -0
  184. package/dist/presentation/subscribers/on-log-stream-closed.d.ts.map +1 -0
  185. package/dist/presentation/subscribers/on-log-stream-closed.js +31 -0
  186. package/dist/presentation/subscribers/on-log-stream-closed.js.map +1 -0
  187. package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts +3 -0
  188. package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts.map +1 -0
  189. package/dist/presentation/subscribers/on-step-attempt-terminated.js +14 -0
  190. package/dist/presentation/subscribers/on-step-attempt-terminated.js.map +1 -0
  191. package/dist/temporal/activities/close-abandoned-streams.d.ts +12 -0
  192. package/dist/temporal/activities/close-abandoned-streams.d.ts.map +1 -0
  193. package/dist/temporal/activities/close-abandoned-streams.js +33 -0
  194. package/dist/temporal/activities/close-abandoned-streams.js.map +1 -0
  195. package/dist/temporal/activities/compact-stream.d.ts +33 -0
  196. package/dist/temporal/activities/compact-stream.d.ts.map +1 -0
  197. package/dist/temporal/activities/compact-stream.js +95 -0
  198. package/dist/temporal/activities/compact-stream.js.map +1 -0
  199. package/dist/temporal/activities/compaction-reconcile.d.ts +16 -0
  200. package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -0
  201. package/dist/temporal/activities/compaction-reconcile.js +52 -0
  202. package/dist/temporal/activities/compaction-reconcile.js.map +1 -0
  203. package/dist/temporal/activities/index.d.ts +14 -0
  204. package/dist/temporal/activities/index.d.ts.map +1 -0
  205. package/dist/temporal/activities/index.js +16 -0
  206. package/dist/temporal/activities/index.js.map +1 -0
  207. package/dist/temporal/activities/reap-stale-open-streams.d.ts +4 -0
  208. package/dist/temporal/activities/reap-stale-open-streams.d.ts.map +1 -0
  209. package/dist/temporal/activities/reap-stale-open-streams.js +11 -0
  210. package/dist/temporal/activities/reap-stale-open-streams.js.map +1 -0
  211. package/dist/temporal/activities/retention-sweep.d.ts +7 -0
  212. package/dist/temporal/activities/retention-sweep.d.ts.map +1 -0
  213. package/dist/temporal/activities/retention-sweep.js +19 -0
  214. package/dist/temporal/activities/retention-sweep.js.map +1 -0
  215. package/dist/temporal/constants.d.ts +7 -0
  216. package/dist/temporal/constants.d.ts.map +1 -0
  217. package/dist/temporal/constants.js +13 -0
  218. package/dist/temporal/constants.js.map +1 -0
  219. package/dist/temporal/workflows/close-abandoned-streams.d.ts +11 -0
  220. package/dist/temporal/workflows/close-abandoned-streams.d.ts.map +1 -0
  221. package/dist/temporal/workflows/close-abandoned-streams.js +22 -0
  222. package/dist/temporal/workflows/close-abandoned-streams.js.map +1 -0
  223. package/dist/temporal/workflows/compact-stream.d.ts +6 -0
  224. package/dist/temporal/workflows/compact-stream.d.ts.map +1 -0
  225. package/dist/temporal/workflows/compact-stream.js +25 -0
  226. package/dist/temporal/workflows/compact-stream.js.map +1 -0
  227. package/dist/temporal/workflows/compaction-reconcile-cron.d.ts +3 -0
  228. package/dist/temporal/workflows/compaction-reconcile-cron.d.ts.map +1 -0
  229. package/dist/temporal/workflows/compaction-reconcile-cron.js +15 -0
  230. package/dist/temporal/workflows/compaction-reconcile-cron.js.map +1 -0
  231. package/dist/temporal/workflows/index.d.ts +6 -0
  232. package/dist/temporal/workflows/index.d.ts.map +1 -0
  233. package/dist/temporal/workflows/index.js +7 -0
  234. package/dist/temporal/workflows/index.js.map +1 -0
  235. package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts +2 -0
  236. package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts.map +1 -0
  237. package/dist/temporal/workflows/reap-stale-open-streams-cron.js +15 -0
  238. package/dist/temporal/workflows/reap-stale-open-streams-cron.js.map +1 -0
  239. package/dist/temporal/workflows/retention-sweep-cron.d.ts +2 -0
  240. package/dist/temporal/workflows/retention-sweep-cron.d.ts.map +1 -0
  241. package/dist/temporal/workflows/retention-sweep-cron.js +23 -0
  242. package/dist/temporal/workflows/retention-sweep-cron.js.map +1 -0
  243. package/dist/tsconfig.test.tsbuildinfo +1 -0
  244. package/drizzle/0000_initial.sql +65 -0
  245. package/drizzle/meta/0000_snapshot.json +529 -0
  246. package/drizzle/meta/_journal.json +13 -0
  247. package/drizzle.config.ts +7 -0
  248. package/package.json +84 -0
  249. package/src/api/object-storage-credentials.test.ts +70 -0
  250. package/src/api/object-storage.test.ts +64 -0
  251. package/src/api/object-storage.ts +226 -0
  252. package/src/config.test.ts +118 -0
  253. package/src/config.ts +130 -0
  254. package/src/core/append-logs.test.ts +563 -0
  255. package/src/core/append-logs.ts +384 -0
  256. package/src/core/budget.test.ts +30 -0
  257. package/src/core/budget.ts +19 -0
  258. package/src/core/close-stream.ts +72 -0
  259. package/src/core/compaction.ts +68 -0
  260. package/src/core/entities/attempt-stream.ts +38 -0
  261. package/src/core/entities/job-accounting.ts +11 -0
  262. package/src/core/entities/log-object.test.ts +25 -0
  263. package/src/core/entities/log-object.ts +18 -0
  264. package/src/core/errors.ts +37 -0
  265. package/src/core/finalize-attempt-stream.test.ts +90 -0
  266. package/src/core/finalize-attempt-stream.ts +59 -0
  267. package/src/core/read-logs.ts +77 -0
  268. package/src/core/reap-stale-open-streams.test.ts +137 -0
  269. package/src/core/reap-stale-open-streams.ts +60 -0
  270. package/src/core/retention.test.ts +388 -0
  271. package/src/core/retention.ts +184 -0
  272. package/src/core/session/claude/rows.ts +223 -0
  273. package/src/core/session/claude-parser.test.ts +199 -0
  274. package/src/core/session/claude-parser.ts +38 -0
  275. package/src/core/session/entry-schema.ts +82 -0
  276. package/src/core/session/object.ts +57 -0
  277. package/src/core/session/parse-session.ts +23 -0
  278. package/src/core/session/pi/message-rows.ts +211 -0
  279. package/src/core/session/pi/metadata.ts +148 -0
  280. package/src/core/session/pi/text.ts +83 -0
  281. package/src/core/session/pi-parser.test.ts +451 -0
  282. package/src/core/session/pi-parser.ts +107 -0
  283. package/src/core/session/rows.ts +38 -0
  284. package/src/core/session/session-record.ts +4 -0
  285. package/src/db/accounting.ts +81 -0
  286. package/src/db/chunks.ts +143 -0
  287. package/src/db/db.ts +22 -0
  288. package/src/db/index.ts +4 -0
  289. package/src/db/schema/attempt-streams.ts +106 -0
  290. package/src/db/schema/chunks.ts +43 -0
  291. package/src/db/schema/common.ts +12 -0
  292. package/src/db/schema/job-accounting.ts +38 -0
  293. package/src/db/schema/outbox.ts +4 -0
  294. package/src/db/streams.test.ts +47 -0
  295. package/src/db/streams.ts +368 -0
  296. package/src/index.ts +77 -0
  297. package/src/metrics/index.ts +1 -0
  298. package/src/metrics/instance.ts +33 -0
  299. package/src/metrics/service.ts +23 -0
  300. package/src/presentation/dto/read-logs.ts +24 -0
  301. package/src/presentation/routes/append-logs.test.ts +279 -0
  302. package/src/presentation/routes/append-logs.ts +66 -0
  303. package/src/presentation/routes/index.ts +27 -0
  304. package/src/presentation/routes/read-logs.test.ts +504 -0
  305. package/src/presentation/routes/read-logs.ts +38 -0
  306. package/src/presentation/subscribers/on-job-terminated.test.ts +64 -0
  307. package/src/presentation/subscribers/on-job-terminated.ts +26 -0
  308. package/src/presentation/subscribers/on-log-stream-closed.test.ts +63 -0
  309. package/src/presentation/subscribers/on-log-stream-closed.ts +26 -0
  310. package/src/presentation/subscribers/on-step-attempt-terminated.ts +16 -0
  311. package/src/temporal/activities/close-abandoned-streams.test.ts +84 -0
  312. package/src/temporal/activities/close-abandoned-streams.ts +30 -0
  313. package/src/temporal/activities/compact-stream.test.ts +221 -0
  314. package/src/temporal/activities/compact-stream.ts +123 -0
  315. package/src/temporal/activities/compaction-reconcile.test.ts +143 -0
  316. package/src/temporal/activities/compaction-reconcile.ts +48 -0
  317. package/src/temporal/activities/index.ts +15 -0
  318. package/src/temporal/activities/reap-stale-open-streams.test.ts +48 -0
  319. package/src/temporal/activities/reap-stale-open-streams.ts +14 -0
  320. package/src/temporal/activities/retention-sweep.test.ts +38 -0
  321. package/src/temporal/activities/retention-sweep.ts +23 -0
  322. package/src/temporal/constants.ts +14 -0
  323. package/src/temporal/workflows/close-abandoned-streams.ts +25 -0
  324. package/src/temporal/workflows/compact-stream.ts +26 -0
  325. package/src/temporal/workflows/compaction-reconcile-cron.ts +14 -0
  326. package/src/temporal/workflows/index.ts +5 -0
  327. package/src/temporal/workflows/reap-stale-open-streams-cron.ts +13 -0
  328. package/src/temporal/workflows/retention-sweep-cron.ts +23 -0
  329. package/test/env.ts +37 -0
  330. package/test/factories/job-accounting.ts +38 -0
  331. package/test/fixtures/closed-stream.ts +53 -0
  332. package/test/fixtures/lease-token.ts +42 -0
  333. package/test/fixtures/ndjson.ts +34 -0
  334. package/test/globalSetup.ts +44 -0
  335. package/test/index.ts +9 -0
  336. package/test/queries.ts +54 -0
  337. package/test/setup.ts +17 -0
  338. package/tsconfig.build.json +9 -0
  339. package/tsconfig.build.tsbuildinfo +1 -0
  340. package/tsconfig.json +3 -0
  341. package/tsconfig.test.json +8 -0
  342. package/vitest.config.ts +13 -0
@@ -0,0 +1,451 @@
1
+ import {parsePiSessionRecord} from './pi-parser.js';
2
+
3
+ const record = (data: unknown, ts = 1) => ({
4
+ ts,
5
+ data: typeof data === 'string' ? data : JSON.stringify(data),
6
+ });
7
+ const meta = (label: string, value: string, inline?: boolean) =>
8
+ inline == null ? {label, value} : {label, value, inline};
9
+
10
+ describe('parsePiSessionRecord', () => {
11
+ test('returns a raw row for malformed JSON', () => {
12
+ const rows = parsePiSessionRecord(record('{not json'));
13
+
14
+ expect(rows).toEqual([
15
+ {kind: 'raw', timestamp: 1, label: 'Malformed session entry', raw: '{not json'},
16
+ ]);
17
+ });
18
+
19
+ test('returns a raw row for an unknown entry type', () => {
20
+ const rows = parsePiSessionRecord(record({type: 'future_entry', payload: {x: 1}}));
21
+
22
+ expect(rows).toEqual([
23
+ {
24
+ kind: 'raw',
25
+ timestamp: 1,
26
+ label: 'Unknown session entry: future_entry',
27
+ raw: '{"type":"future_entry","payload":{"x":1}}',
28
+ },
29
+ ]);
30
+ });
31
+
32
+ test('expands assistant text, thinking, and tool calls into ordered rows', () => {
33
+ const rows = parsePiSessionRecord(
34
+ record({
35
+ type: 'message',
36
+ message: {
37
+ role: 'assistant',
38
+ model: 'claude-opus-4',
39
+ content: [
40
+ {type: 'text', text: 'I will edit the file.'},
41
+ {type: 'thinking', text: 'Need to inspect the module.'},
42
+ {type: 'toolCall', id: 'call-1', name: 'read_file', arguments: {path: 'src/a.ts'}},
43
+ ],
44
+ },
45
+ }),
46
+ );
47
+
48
+ expect(rows).toEqual([
49
+ {
50
+ kind: 'message',
51
+ timestamp: 1,
52
+ role: 'assistant',
53
+ label: 'assistant',
54
+ meta: [meta('model', 'claude-opus-4')],
55
+ text: 'I will edit the file.',
56
+ terminalFailure: false,
57
+ },
58
+ {kind: 'thinking', timestamp: 1, text: 'Need to inspect the module.'},
59
+ {
60
+ kind: 'tool-call',
61
+ timestamp: 1,
62
+ id: 'call-1',
63
+ name: 'read_file',
64
+ input: '{\n "path": "src/a.ts"\n}',
65
+ },
66
+ ]);
67
+ });
68
+
69
+ test('preserves interleaved assistant text and thinking block order', () => {
70
+ const rows = parsePiSessionRecord(
71
+ record({
72
+ type: 'message',
73
+ message: {
74
+ role: 'assistant',
75
+ content: [
76
+ {type: 'text', text: 'First visible update.'},
77
+ {type: 'thinking', text: 'First private thought.'},
78
+ {type: 'text', text: 'Second visible update.'},
79
+ {type: 'toolCall', id: 'call-1', name: 'read_file', arguments: {path: 'src/a.ts'}},
80
+ {type: 'thinking', text: 'Thought after tool call.'},
81
+ {type: 'text', text: 'Final visible update.'},
82
+ ],
83
+ },
84
+ }),
85
+ );
86
+
87
+ expect(rows).toEqual([
88
+ {
89
+ kind: 'message',
90
+ timestamp: 1,
91
+ role: 'assistant',
92
+ label: 'assistant',
93
+ meta: [],
94
+ text: 'First visible update.',
95
+ terminalFailure: false,
96
+ },
97
+ {kind: 'thinking', timestamp: 1, text: 'First private thought.'},
98
+ {
99
+ kind: 'message',
100
+ timestamp: 1,
101
+ role: 'assistant',
102
+ label: 'assistant',
103
+ meta: [],
104
+ text: 'Second visible update.',
105
+ terminalFailure: false,
106
+ },
107
+ {
108
+ kind: 'tool-call',
109
+ timestamp: 1,
110
+ id: 'call-1',
111
+ name: 'read_file',
112
+ input: '{\n "path": "src/a.ts"\n}',
113
+ },
114
+ {kind: 'thinking', timestamp: 1, text: 'Thought after tool call.'},
115
+ {
116
+ kind: 'message',
117
+ timestamp: 1,
118
+ role: 'assistant',
119
+ label: 'assistant',
120
+ meta: [],
121
+ text: 'Final visible update.',
122
+ terminalFailure: false,
123
+ },
124
+ ]);
125
+ });
126
+
127
+ test.each([
128
+ ['success', false],
129
+ ['error', true],
130
+ ])('expands a tool result with %s state', (_label, isError) => {
131
+ const rows = parsePiSessionRecord(
132
+ record({
133
+ type: 'message',
134
+ message: {
135
+ toolCallId: 'call-1',
136
+ toolName: 'read_file',
137
+ content: [{type: 'text', text: 'file contents'}],
138
+ isError,
139
+ },
140
+ }),
141
+ );
142
+
143
+ expect(rows).toEqual([
144
+ {
145
+ kind: 'tool-result',
146
+ timestamp: 1,
147
+ toolCallId: 'call-1',
148
+ toolName: 'read_file',
149
+ output: 'file contents',
150
+ isError,
151
+ },
152
+ ]);
153
+ });
154
+
155
+ test.each([
156
+ [{type: 'session', id: 'session-1'}, 'Session started', 'session-1', []],
157
+ [
158
+ {type: 'session', version: 2, id: 'session-1', cwd: '/workspace'},
159
+ 'Session started',
160
+ 'v2 · session-1 · /workspace',
161
+ [],
162
+ ],
163
+ [{type: 'thinking_level_change', thinkingLevel: 'high'}, 'Thinking level changed', 'high', []],
164
+ [
165
+ {type: 'model_change', modelId: 'gpt-5-codex', provider: 'openai'},
166
+ 'Model changed',
167
+ 'gpt-5-codex · openai',
168
+ [],
169
+ ],
170
+ [
171
+ {type: 'compaction', summary: 'summarized old context', tokensBefore: 12_345},
172
+ 'Context compacted',
173
+ 'summarized old context',
174
+ [meta('tokens before', '12.3K tokens')],
175
+ ],
176
+ [
177
+ {type: 'label', label: 'Implement tests', targetId: 'entry-1'},
178
+ 'Label',
179
+ 'Implement tests',
180
+ [meta('target', 'entry-1', false)],
181
+ ],
182
+ ])('expands lifecycle entry %#', (entry, label, detail, meta) => {
183
+ const rows = parsePiSessionRecord(record(entry));
184
+
185
+ expect(rows).toEqual([
186
+ {
187
+ kind: 'lifecycle',
188
+ timestamp: 1,
189
+ label,
190
+ detail,
191
+ meta,
192
+ tone: 'default',
193
+ terminalFailure: false,
194
+ },
195
+ ]);
196
+ });
197
+
198
+ test('renders message content blocks without exposing image data', () => {
199
+ const rows = parsePiSessionRecord(
200
+ record({
201
+ type: 'message',
202
+ message: {
203
+ role: 'user',
204
+ content: [
205
+ {type: 'text', text: 'Inspect this screenshot.'},
206
+ {type: 'image', mimeType: 'image/png', data: 'base64-payload'},
207
+ ],
208
+ },
209
+ }),
210
+ );
211
+
212
+ expect(rows).toEqual([
213
+ {
214
+ kind: 'message',
215
+ timestamp: 1,
216
+ role: 'user',
217
+ label: 'user',
218
+ meta: [],
219
+ text: 'Inspect this screenshot.\n\n[image/png image]',
220
+ terminalFailure: false,
221
+ },
222
+ ]);
223
+ });
224
+
225
+ test('renders bash execution messages without falling back to raw JSON', () => {
226
+ const rows = parsePiSessionRecord(
227
+ record({
228
+ type: 'message',
229
+ message: {
230
+ role: 'bashExecution',
231
+ command: 'pnpm test',
232
+ output: '1 failed',
233
+ exitCode: 1,
234
+ cancelled: false,
235
+ truncated: true,
236
+ fullOutputPath: '/tmp/output.log',
237
+ },
238
+ }),
239
+ );
240
+
241
+ expect(rows).toEqual([
242
+ {
243
+ kind: 'message',
244
+ timestamp: 1,
245
+ role: 'bash-execution',
246
+ label: 'bash execution',
247
+ meta: [
248
+ meta('exit', '1'),
249
+ meta('truncated', 'yes'),
250
+ meta('full output', '/tmp/output.log', false),
251
+ ],
252
+ text: '$ pnpm test\n1 failed',
253
+ terminalFailure: false,
254
+ },
255
+ ]);
256
+ });
257
+
258
+ test.each([
259
+ [
260
+ {role: 'branchSummary', summary: 'branched for fix', fromId: 'entry-1'},
261
+ 'branch-summary',
262
+ 'branch summary',
263
+ [meta('from', 'entry-1')],
264
+ 'branched for fix',
265
+ ],
266
+ [
267
+ {role: 'compactionSummary', summary: 'summarized history', tokensBefore: 42_000},
268
+ 'compaction-summary',
269
+ 'compaction summary',
270
+ [meta('tokens before', '42K tokens')],
271
+ 'summarized history',
272
+ ],
273
+ [
274
+ {role: 'custom', customType: 'extension', content: 'extension output', display: true},
275
+ 'custom',
276
+ 'custom',
277
+ [meta('type', 'extension'), meta('display', 'on')],
278
+ 'extension output',
279
+ ],
280
+ ])('renders extended message role %#', (message, role, label, meta, text) => {
281
+ const rows = parsePiSessionRecord(record({type: 'message', message}));
282
+
283
+ expect(rows).toEqual([
284
+ {kind: 'message', timestamp: 1, role, label, meta, text, terminalFailure: false},
285
+ ]);
286
+ });
287
+
288
+ test('marks assistant terminal errors as failure anchors', () => {
289
+ const rows = parsePiSessionRecord(
290
+ record({
291
+ type: 'message',
292
+ message: {
293
+ role: 'assistant',
294
+ content: [{type: 'text', text: 'I cannot continue.'}],
295
+ stopReason: 'error',
296
+ },
297
+ }),
298
+ );
299
+
300
+ expect(rows[0]).toMatchObject({kind: 'message', terminalFailure: true});
301
+ });
302
+
303
+ test('adds a failure anchor for thinking-only assistant terminal errors', () => {
304
+ const rows = parsePiSessionRecord(
305
+ record({
306
+ type: 'message',
307
+ message: {
308
+ role: 'assistant',
309
+ content: [{type: 'thinking', text: 'No visible response before failure.'}],
310
+ stopReason: 'error',
311
+ },
312
+ }),
313
+ );
314
+
315
+ expect(rows).toEqual([
316
+ {kind: 'thinking', timestamp: 1, text: 'No visible response before failure.'},
317
+ {
318
+ kind: 'message',
319
+ timestamp: 1,
320
+ role: 'assistant',
321
+ label: 'assistant',
322
+ meta: [meta('stop', 'error')],
323
+ text: 'Assistant stopped with an error.',
324
+ terminalFailure: true,
325
+ },
326
+ ]);
327
+ });
328
+
329
+ test('expands a tool result with no id', () => {
330
+ const rows = parsePiSessionRecord(
331
+ record({
332
+ type: 'message',
333
+ message: {role: 'tool', toolName: 'read_file', content: [{type: 'text', text: 'data'}]},
334
+ }),
335
+ );
336
+
337
+ expect(rows).toEqual([
338
+ {
339
+ kind: 'tool-result',
340
+ timestamp: 1,
341
+ toolCallId: null,
342
+ toolName: 'read_file',
343
+ output: 'data',
344
+ isError: false,
345
+ },
346
+ ]);
347
+ });
348
+
349
+ test('expands a tool call with no id', () => {
350
+ const rows = parsePiSessionRecord(
351
+ record({
352
+ type: 'message',
353
+ message: {
354
+ role: 'assistant',
355
+ content: [{type: 'tool_call', name: 'run', arguments: {cmd: 'ls'}}],
356
+ },
357
+ }),
358
+ );
359
+
360
+ expect(rows).toEqual([
361
+ {kind: 'tool-call', timestamp: 1, id: null, name: 'run', input: '{\n "cmd": "ls"\n}'},
362
+ ]);
363
+ });
364
+
365
+ test('falls back to the raw message when assistant content has no renderable blocks', () => {
366
+ const rows = parsePiSessionRecord(
367
+ record({type: 'message', message: {role: 'assistant', content: [{type: 'image'}]}}),
368
+ );
369
+
370
+ expect(rows).toHaveLength(1);
371
+ expect(rows[0]).toMatchObject({kind: 'message', role: 'assistant'});
372
+ });
373
+
374
+ test('renders a branch summary as a system message row', () => {
375
+ const rows = parsePiSessionRecord(
376
+ record({type: 'branch_summary', summary: 'merged main', fromId: 'entry-1'}),
377
+ );
378
+
379
+ expect(rows).toEqual([
380
+ {
381
+ kind: 'message',
382
+ timestamp: 1,
383
+ role: 'system',
384
+ label: 'branch summary',
385
+ meta: [meta('from', 'entry-1')],
386
+ text: 'merged main',
387
+ terminalFailure: false,
388
+ },
389
+ ]);
390
+ });
391
+
392
+ test('renders a custom entry as a message row', () => {
393
+ const rows = parsePiSessionRecord(
394
+ record({type: 'custom', customType: 'extension', data: {ok: true}}),
395
+ );
396
+
397
+ expect(rows).toEqual([
398
+ {
399
+ kind: 'message',
400
+ timestamp: 1,
401
+ role: 'custom',
402
+ label: 'custom',
403
+ meta: [meta('type', 'extension')],
404
+ text: '{\n "ok": true\n}',
405
+ terminalFailure: false,
406
+ },
407
+ ]);
408
+ });
409
+
410
+ test('renders a custom message entry from content', () => {
411
+ const rows = parsePiSessionRecord(
412
+ record({type: 'custom_message', customType: 'extension', content: 'hello there'}),
413
+ );
414
+
415
+ expect(rows).toEqual([
416
+ {
417
+ kind: 'message',
418
+ timestamp: 1,
419
+ role: 'custom',
420
+ label: 'custom message',
421
+ meta: [meta('type', 'extension')],
422
+ text: 'hello there',
423
+ terminalFailure: false,
424
+ },
425
+ ]);
426
+ });
427
+
428
+ test('renders a session_info entry as a lifecycle row', () => {
429
+ const rows = parsePiSessionRecord(record({type: 'session_info', message: 'resumed'}));
430
+
431
+ expect(rows).toEqual([
432
+ {
433
+ kind: 'lifecycle',
434
+ timestamp: 1,
435
+ label: 'Session info',
436
+ detail: 'resumed',
437
+ meta: [],
438
+ tone: 'default',
439
+ terminalFailure: false,
440
+ },
441
+ ]);
442
+ });
443
+
444
+ test('returns an unsupported raw row when the entry has no type', () => {
445
+ const rows = parsePiSessionRecord(record({payload: {x: 1}}));
446
+
447
+ expect(rows).toEqual([
448
+ {kind: 'raw', timestamp: 1, label: 'Unsupported entry', raw: '{"payload":{"x":1}}'},
449
+ ]);
450
+ });
451
+ });
@@ -0,0 +1,107 @@
1
+ import type {SessionViewRow} from '@shipfox/api-logs-dto';
2
+ import {parseSessionEntry, sessionMessageEntrySchema} from './entry-schema.js';
3
+ import {toJson} from './object.js';
4
+ import {expandMessageEntry} from './pi/message-rows.js';
5
+ import {
6
+ branchSummaryDetail,
7
+ branchSummaryMeta,
8
+ compactionDetail,
9
+ compactionMeta,
10
+ customEntryLabel,
11
+ customEntryMeta,
12
+ customEntryText,
13
+ entryDetail,
14
+ labelDetail,
15
+ labelMeta,
16
+ modelChangeDetail,
17
+ sessionDetail,
18
+ } from './pi/metadata.js';
19
+ import {lifecycleRow, messageRow, rawRecordRow} from './rows.js';
20
+ import type {AgentSessionRecord} from './session-record.js';
21
+
22
+ export type {AgentSessionRecord} from './session-record.js';
23
+
24
+ export function parsePiSessionRecord(record: AgentSessionRecord): readonly SessionViewRow[] {
25
+ const parsed = parseSessionEntry(record.data);
26
+ if (!parsed.ok) {
27
+ return [
28
+ rawRecordRow(
29
+ record,
30
+ parsed.reason === 'invalid-json' ? 'Malformed session entry' : 'Unsupported entry',
31
+ ),
32
+ ];
33
+ }
34
+
35
+ const entry = parsed.entry;
36
+ switch (entry.type) {
37
+ case 'message': {
38
+ const parsedEntry = sessionMessageEntrySchema.safeParse(entry);
39
+ if (!parsedEntry.success) {
40
+ return [
41
+ {kind: 'raw', timestamp: record.ts, label: 'Unsupported message entry', raw: record.data},
42
+ ];
43
+ }
44
+ return expandMessageEntry(record.ts, parsedEntry.data.message);
45
+ }
46
+ case 'session':
47
+ return [lifecycleRow(record.ts, 'Session started', sessionDetail(entry), 'default', false)];
48
+ case 'session_info':
49
+ return [lifecycleRow(record.ts, 'Session info', entryDetail(entry), 'default', false)];
50
+ case 'thinking_level_change':
51
+ return [
52
+ lifecycleRow(record.ts, 'Thinking level changed', entryDetail(entry), 'default', false),
53
+ ];
54
+ case 'model_change':
55
+ return [lifecycleRow(record.ts, 'Model changed', modelChangeDetail(entry), 'default', false)];
56
+ case 'compaction':
57
+ return [
58
+ lifecycleRow(
59
+ record.ts,
60
+ 'Context compacted',
61
+ compactionDetail(entry),
62
+ 'default',
63
+ false,
64
+ compactionMeta(entry),
65
+ ),
66
+ ];
67
+ case 'branch_summary':
68
+ return [
69
+ messageRow(
70
+ record.ts,
71
+ 'system',
72
+ 'branch summary',
73
+ branchSummaryDetail(entry) ?? toJson(entry),
74
+ false,
75
+ branchSummaryMeta(entry),
76
+ ),
77
+ ];
78
+ case 'custom':
79
+ return [
80
+ messageRow(
81
+ record.ts,
82
+ 'custom',
83
+ customEntryLabel('custom'),
84
+ customEntryText(entry) ?? toJson(entry),
85
+ false,
86
+ customEntryMeta(entry),
87
+ ),
88
+ ];
89
+ case 'custom_message':
90
+ return [
91
+ messageRow(
92
+ record.ts,
93
+ 'custom',
94
+ customEntryLabel('custom message'),
95
+ customEntryText(entry) ?? toJson(entry),
96
+ false,
97
+ customEntryMeta(entry),
98
+ ),
99
+ ];
100
+ case 'label':
101
+ return [
102
+ lifecycleRow(record.ts, 'Label', labelDetail(entry), 'default', false, labelMeta(entry)),
103
+ ];
104
+ default:
105
+ return [rawRecordRow(record, `Unknown session entry: ${entry.type}`)];
106
+ }
107
+ }
@@ -0,0 +1,38 @@
1
+ import type {
2
+ SessionViewLifecycleRow,
3
+ SessionViewMessageRow,
4
+ SessionViewRow,
5
+ SessionViewRowMeta,
6
+ SessionViewThinkingRow,
7
+ } from '@shipfox/api-logs-dto';
8
+ import type {AgentSessionRecord} from './session-record.js';
9
+
10
+ export function messageRow(
11
+ timestamp: number,
12
+ role: string,
13
+ label: string,
14
+ text: string,
15
+ terminalFailure: boolean,
16
+ meta: readonly SessionViewRowMeta[] = [],
17
+ ): SessionViewMessageRow {
18
+ return {kind: 'message', timestamp, role, label, meta, text, terminalFailure};
19
+ }
20
+
21
+ export function thinkingRow(timestamp: number, text: string): SessionViewThinkingRow {
22
+ return {kind: 'thinking', timestamp, text};
23
+ }
24
+
25
+ export function lifecycleRow(
26
+ timestamp: number,
27
+ label: string,
28
+ detail: string | null,
29
+ tone: SessionViewLifecycleRow['tone'],
30
+ terminalFailure: boolean,
31
+ meta: readonly SessionViewRowMeta[] = [],
32
+ ): SessionViewLifecycleRow {
33
+ return {kind: 'lifecycle', timestamp, label, detail, meta, tone, terminalFailure};
34
+ }
35
+
36
+ export function rawRecordRow(record: AgentSessionRecord, label: string): SessionViewRow {
37
+ return {kind: 'raw', timestamp: record.ts, label, raw: record.data};
38
+ }
@@ -0,0 +1,4 @@
1
+ export interface AgentSessionRecord {
2
+ data: string;
3
+ ts: number;
4
+ }
@@ -0,0 +1,81 @@
1
+ import {and, eq, isNull, lt, sql} from 'drizzle-orm';
2
+ import type {Transaction} from './db.js';
3
+ import {jobAccounting} from './schema/job-accounting.js';
4
+
5
+ /** Creates the per-job accounting row on first append; a no-op once it exists. */
6
+ export async function ensureJobAccounting(
7
+ tx: Transaction,
8
+ params: {jobId: string; workspaceId: string},
9
+ ): Promise<void> {
10
+ await tx
11
+ .insert(jobAccounting)
12
+ .values({jobId: params.jobId, workspaceId: params.workspaceId})
13
+ .onConflictDoNothing({target: jobAccounting.jobId});
14
+ }
15
+
16
+ export interface AccrualResult {
17
+ used: number;
18
+ startedAt: Date;
19
+ }
20
+
21
+ /**
22
+ * Atomically adds `delta` stored bytes to the job's accrual, returning the new
23
+ * total and the budget clock origin. Returns null when the job is already capped
24
+ * (the `capped_at IS NULL` guard matched no row), so the caller drops the append.
25
+ */
26
+ export async function accrueStoredBytes(
27
+ tx: Transaction,
28
+ params: {jobId: string; delta: number},
29
+ ): Promise<AccrualResult | null> {
30
+ const [row] = await tx
31
+ .update(jobAccounting)
32
+ .set({
33
+ storedBytesUsed: sql`${jobAccounting.storedBytesUsed} + ${params.delta}`,
34
+ updatedAt: sql`now()`,
35
+ })
36
+ .where(and(eq(jobAccounting.jobId, params.jobId), isNull(jobAccounting.cappedAt)))
37
+ .returning({used: jobAccounting.storedBytesUsed, startedAt: jobAccounting.startedAt});
38
+
39
+ return row ?? null;
40
+ }
41
+
42
+ /** Claims the cap for a job. Returns true for the single caller that wins the race. */
43
+ export async function claimCap(tx: Transaction, jobId: string): Promise<boolean> {
44
+ const [row] = await tx
45
+ .update(jobAccounting)
46
+ .set({cappedAt: sql`now()`, updatedAt: sql`now()`})
47
+ .where(and(eq(jobAccounting.jobId, jobId), isNull(jobAccounting.cappedAt)))
48
+ .returning({jobId: jobAccounting.jobId});
49
+
50
+ return Boolean(row);
51
+ }
52
+
53
+ export async function isJobCapped(tx: Transaction, jobId: string): Promise<boolean> {
54
+ const [row] = await tx
55
+ .select({cappedAt: jobAccounting.cappedAt})
56
+ .from(jobAccounting)
57
+ .where(eq(jobAccounting.jobId, jobId));
58
+
59
+ return Boolean(row?.cappedAt);
60
+ }
61
+
62
+ /**
63
+ * Prunes accounting only after the job has no recent budget activity. A live job re-touches
64
+ * `updated_at` on append, so this guard keeps retention from resetting its cap state.
65
+ */
66
+ export async function deleteJobAccounting(
67
+ tx: Transaction,
68
+ params: {jobId: string; retentionDays: number},
69
+ ): Promise<{deleted: boolean}> {
70
+ const [row] = await tx
71
+ .delete(jobAccounting)
72
+ .where(
73
+ and(
74
+ eq(jobAccounting.jobId, params.jobId),
75
+ lt(jobAccounting.updatedAt, sql`now() - make_interval(days => ${params.retentionDays})`),
76
+ ),
77
+ )
78
+ .returning({jobId: jobAccounting.jobId});
79
+
80
+ return {deleted: Boolean(row)};
81
+ }