@gobing-ai/spur 0.4.17 → 0.4.19

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 (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/workflows/task-pipeline.yaml +12 -7
  3. package/package.json +1 -1
  4. package/plugins/sp/README.md +10 -6
  5. package/plugins/sp/agents/super-reviewer.md +3 -1
  6. package/plugins/sp/commands/dev-changelog.md +1 -1
  7. package/plugins/sp/plugin.json +1 -1
  8. package/plugins/sp/skills/spur-cli/SKILL.md +2 -2
  9. package/plugins/sp/skills/spur-cli/references/message.md +7 -5
  10. package/plugins/sp/skills/spur-dev/references/dev-operations.md +3 -2
  11. package/spur.js +1715 -1157
  12. package/web/_astro/{BoardApp.mWyrWCAR.js → BoardApp.CNF3sCnm.js} +1 -1
  13. package/web/_astro/BoardApp.DY8fwPfw.js +322 -0
  14. package/web/_astro/{TaskDetail.CtAXuiv8.js → TaskDetail.8z3DgYux.js} +1 -1
  15. package/web/_astro/{arc.Duf4u60i.js → arc.CFEYbpFT.js} +1 -1
  16. package/web/_astro/{architectureDiagram-3BPJPVTR.JoymtqxN.js → architectureDiagram-3BPJPVTR.3eDvG_X8.js} +1 -1
  17. package/web/_astro/{blockDiagram-GPEHLZMM.BvmhcioC.js → blockDiagram-GPEHLZMM.CMbLzMpk.js} +1 -1
  18. package/web/_astro/{c4Diagram-AAUBKEIU.CfaWkgoQ.js → c4Diagram-AAUBKEIU.2lZW8nZ-.js} +1 -1
  19. package/web/_astro/channel.CdQ8ZO1m.js +1 -0
  20. package/web/_astro/{chunk-2J33WTMH.B7uR02cD.js → chunk-2J33WTMH.BtM1eI3D.js} +1 -1
  21. package/web/_astro/{chunk-4BX2VUAB.SwiEauYR.js → chunk-4BX2VUAB.CKeckzAf.js} +1 -1
  22. package/web/_astro/{chunk-55IACEB6.6ZRCK8PO.js → chunk-55IACEB6.BBmHet8U.js} +1 -1
  23. package/web/_astro/{chunk-727SXJPM.Cxra_G3N.js → chunk-727SXJPM.Bi8v7M_x.js} +1 -1
  24. package/web/_astro/{chunk-AQP2D5EJ.Cxd7gZeX.js → chunk-AQP2D5EJ.DOcBJyb6.js} +1 -1
  25. package/web/_astro/{chunk-FMBD7UC4.DCLhFkVc.js → chunk-FMBD7UC4.C-EdUBD7.js} +1 -1
  26. package/web/_astro/{chunk-ND2GUHAM.B9AmKcv6.js → chunk-ND2GUHAM.IqTyWQhk.js} +1 -1
  27. package/web/_astro/{chunk-QZHKN3VN.DOjCvl6S.js → chunk-QZHKN3VN.a2DShvIL.js} +1 -1
  28. package/web/_astro/{classDiagram-4FO5ZUOK.BMiT_mNf.js → classDiagram-4FO5ZUOK.CQBnvIjk.js} +1 -1
  29. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BMiT_mNf.js → classDiagram-v2-Q7XG4LA2.CQBnvIjk.js} +1 -1
  30. package/web/_astro/{cose-bilkent-S5V4N54A.mLBCEGjI.js → cose-bilkent-S5V4N54A.BW9Ryz7c.js} +1 -1
  31. package/web/_astro/{cynefin-OW5HDTMX.CjDAY0qn.js → cynefin-OW5HDTMX.BPhLeSep.js} +1 -1
  32. package/web/_astro/{dagre-BM42HDAG._G_sP8fP.js → dagre-BM42HDAG._oD9MsDB.js} +1 -1
  33. package/web/_astro/{diagram-2AECGRRQ.39OcfSuz.js → diagram-2AECGRRQ.iciSOlxY.js} +1 -1
  34. package/web/_astro/{diagram-5GNKFQAL.D7v_eNC4.js → diagram-5GNKFQAL.ZzqeC45Q.js} +1 -1
  35. package/web/_astro/{diagram-KO2AKTUF.Da1x-lhg.js → diagram-KO2AKTUF.CMqWGOYB.js} +1 -1
  36. package/web/_astro/{diagram-LMA3HP47.C8EnzbEb.js → diagram-LMA3HP47.B5MPN99W.js} +1 -1
  37. package/web/_astro/{diagram-OG6HWLK6.DPnBj69F.js → diagram-OG6HWLK6.Zgi-ANAF.js} +1 -1
  38. package/web/_astro/{erDiagram-TEJ5UH35.CXMu2TIh.js → erDiagram-TEJ5UH35.Bd5aeIyR.js} +1 -1
  39. package/web/_astro/{flowDiagram-I6XJVG4X.ZklePVtQ.js → flowDiagram-I6XJVG4X.B8LBo_PH.js} +1 -1
  40. package/web/_astro/{ganttDiagram-6RSMTGT7.BhYOkdqk.js → ganttDiagram-6RSMTGT7.Di2HLHk4.js} +1 -1
  41. package/web/_astro/{gitGraphDiagram-PVQCEYII.CADyX8a1.js → gitGraphDiagram-PVQCEYII.BqDuU6fc.js} +1 -1
  42. package/web/_astro/index.DVDW9-eD.css +1 -0
  43. package/web/_astro/{infoDiagram-5YYISTIA.BsdmwjHt.js → infoDiagram-5YYISTIA.C_hw98uF.js} +1 -1
  44. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CzjZ3Crd.js → ishikawaDiagram-YF4QCWOH.uK7JK9Kk.js} +1 -1
  45. package/web/_astro/{journeyDiagram-JHISSGLW.Bxj-ZH_t.js → journeyDiagram-JHISSGLW.D03MNAmn.js} +1 -1
  46. package/web/_astro/{kanban-definition-UN3LZRKU.Cbg3wVvX.js → kanban-definition-UN3LZRKU.DuiDtKhq.js} +1 -1
  47. package/web/_astro/{linear.DJNUrBI9.js → linear.Der6_z5m.js} +1 -1
  48. package/web/_astro/{mermaid.core.DVHQV3SF.js → mermaid.core.CaxkEgwh.js} +4 -4
  49. package/web/_astro/{mindmap-definition-RKZ34NQL.DyomosRU.js → mindmap-definition-RKZ34NQL.DecT082u.js} +1 -1
  50. package/web/_astro/{pieDiagram-4H26LBE5.DajtFknN.js → pieDiagram-4H26LBE5.DuAEfZXh.js} +1 -1
  51. package/web/_astro/{quadrantDiagram-W4KKPZXB.Bjq6SjGe.js → quadrantDiagram-W4KKPZXB.CWXczU1k.js} +1 -1
  52. package/web/_astro/{requirementDiagram-4Y6WPE33.ubBVqGLP.js → requirementDiagram-4Y6WPE33.BsAHUsR6.js} +1 -1
  53. package/web/_astro/{sankeyDiagram-5OEKKPKP.I4q46xd-.js → sankeyDiagram-5OEKKPKP.ghBqvXN8.js} +1 -1
  54. package/web/_astro/{sequenceDiagram-3UESZ5HK.DsCgZIa8.js → sequenceDiagram-3UESZ5HK.BKbwb7ad.js} +1 -1
  55. package/web/_astro/{stateDiagram-AJRCARHV.DcYjYSqT.js → stateDiagram-AJRCARHV.D5geOKAc.js} +1 -1
  56. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dt8yPqiS.js → stateDiagram-v2-BHNVJYJU.Dc4Sjs4R.js} +1 -1
  57. package/web/_astro/{timeline-definition-PNZ67QCA.0YTkoa5F.js → timeline-definition-PNZ67QCA.ve2dW2tR.js} +1 -1
  58. package/web/_astro/{vennDiagram-CIIHVFJN.BNl0l1hs.js → vennDiagram-CIIHVFJN.BdJumy3f.js} +1 -1
  59. package/web/_astro/{wardleyDiagram-YWT4CUSO.BYSX6cfn.js → wardleyDiagram-YWT4CUSO.X8HgtN8V.js} +1 -1
  60. package/web/_astro/{xychartDiagram-2RQKCTM6.DzxxpixK.js → xychartDiagram-2RQKCTM6.C30VVwQ6.js} +1 -1
  61. package/web/board-runtime.json +0 -6
  62. package/web/index.html +2 -2
  63. package/web/_astro/BoardApp.CC7Mwswv.js +0 -324
  64. package/web/_astro/channel.DtEPXXcH.js +0 -1
  65. package/web/_astro/index.DiC1a6lh.css +0 -1
package/spur.js CHANGED
@@ -34977,202 +34977,6 @@ var init_entity_dao = __esm(() => {
34977
34977
  };
34978
34978
  });
34979
34979
 
34980
- // ../../node_modules/.bun/@gobing-ai+ts-db@0.5.32+3ceefb3da453b19a/node_modules/@gobing-ai/ts-db/dist/inbox-message-dao.js
34981
- function isPromiseLike(value) {
34982
- return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
34983
- }
34984
- function isRequestKeyUniqueViolation(error) {
34985
- const msg = error instanceof Error ? error.message : String(error);
34986
- return msg.includes("UNIQUE constraint failed") && (msg.includes("idx_inbox_messages_request_key") || msg.includes("inbox_messages.request_key"));
34987
- }
34988
- var RequestKeyConflictError, InboxMessageDao;
34989
- var init_inbox_message_dao = __esm(() => {
34990
- init_drizzle_orm();
34991
- init_entity_dao();
34992
- init_inbox_messages();
34993
- RequestKeyConflictError = class RequestKeyConflictError extends Error {
34994
- requestKey;
34995
- existingId;
34996
- constructor(requestKey, existingId) {
34997
- super(`Request key conflict: key is already bound to another payload` + (existingId ? ` (existing message id: ${existingId})` : ""));
34998
- this.name = "RequestKeyConflictError";
34999
- this.requestKey = requestKey;
35000
- this.existingId = existingId;
35001
- }
35002
- };
35003
- InboxMessageDao = class InboxMessageDao extends EntityDao {
35004
- events;
35005
- constructor(adapter, options = {}) {
35006
- super(adapter, inboxMessages, [inboxMessages.id], "inbox_messages");
35007
- this.events = options.events;
35008
- }
35009
- async enqueue(fromId, toId, body, inReplyTo) {
35010
- const id = crypto.randomUUID();
35011
- const now = this.now();
35012
- await this.create({
35013
- id,
35014
- fromId,
35015
- toId,
35016
- body,
35017
- status: "queued",
35018
- injectAttempts: 0,
35019
- createdAt: now,
35020
- updatedAt: now,
35021
- ...inReplyTo !== undefined ? { inReplyTo } : {}
35022
- });
35023
- this.emitEvent("message.enqueued", {
35024
- id,
35025
- fromId,
35026
- toId,
35027
- ...inReplyTo !== undefined ? { inReplyTo } : {},
35028
- timestamp: now
35029
- });
35030
- return id;
35031
- }
35032
- async enqueueIdempotent(fromId, toId, body, requestKey, inReplyTo) {
35033
- const id = crypto.randomUUID();
35034
- const now = this.now();
35035
- try {
35036
- await this.create({
35037
- id,
35038
- fromId,
35039
- toId,
35040
- body,
35041
- status: "queued",
35042
- injectAttempts: 0,
35043
- requestKey,
35044
- createdAt: now,
35045
- updatedAt: now,
35046
- ...inReplyTo !== undefined ? { inReplyTo } : {}
35047
- });
35048
- } catch (error) {
35049
- if (isRequestKeyUniqueViolation(error)) {
35050
- const existing = await this.findBy(inboxMessages.requestKey, requestKey);
35051
- if (existing && existing.body === body && existing.toId === toId) {
35052
- return { id: existing.id, replayed: true };
35053
- }
35054
- throw new RequestKeyConflictError(requestKey, existing?.id ?? "");
35055
- }
35056
- throw error;
35057
- }
35058
- this.emitEvent("message.enqueued", {
35059
- id,
35060
- fromId,
35061
- toId,
35062
- ...inReplyTo !== undefined ? { inReplyTo } : {},
35063
- timestamp: now
35064
- });
35065
- return { id, replayed: false };
35066
- }
35067
- async drainPending(toId, options) {
35068
- const limit = options?.limit ?? 100;
35069
- if (!Number.isInteger(limit) || limit <= 0) {
35070
- throw new RangeError(`drainPending limit must be a positive integer; received ${limit}`);
35071
- }
35072
- const now = this.now();
35073
- const rows = await this.db.update(inboxMessages).set({
35074
- status: "injected",
35075
- injectAttempts: sql`${inboxMessages.injectAttempts} + 1`,
35076
- updatedAt: now
35077
- }).where(sql`${inboxMessages.id} IN (
35078
- SELECT id
35079
- FROM ${inboxMessages}
35080
- WHERE to_id = ${toId}
35081
- AND status = 'queued'
35082
- ORDER BY created_at
35083
- LIMIT ${limit}
35084
- )
35085
- AND ${inboxMessages.status} = 'queued'`).returning();
35086
- for (const row of rows) {
35087
- this.emitEvent("message.injected", {
35088
- id: row.id,
35089
- fromId: row.fromId,
35090
- toId: row.toId,
35091
- inReplyTo: row.inReplyTo,
35092
- injectAttempts: row.injectAttempts,
35093
- timestamp: now
35094
- });
35095
- }
35096
- return rows;
35097
- }
35098
- async markDelivered(msgId) {
35099
- const now = this.now();
35100
- const updated = await this.update(msgId, {
35101
- status: "delivered",
35102
- deliveredAt: now
35103
- });
35104
- if (updated) {
35105
- this.emitEvent("message.delivered", {
35106
- id: updated.id,
35107
- fromId: updated.fromId,
35108
- toId: updated.toId,
35109
- deliveredAt: now,
35110
- timestamp: now
35111
- });
35112
- }
35113
- }
35114
- async markFailed(msgId, error) {
35115
- const updated = await this.update(msgId, {
35116
- status: "failed",
35117
- injectError: error
35118
- });
35119
- if (updated) {
35120
- this.emitEvent("message.failed", {
35121
- id: updated.id,
35122
- fromId: updated.fromId,
35123
- toId: updated.toId,
35124
- error,
35125
- timestamp: updated.updatedAt
35126
- });
35127
- }
35128
- }
35129
- async release(msgIds) {
35130
- if (msgIds.length === 0)
35131
- return 0;
35132
- const now = this.now();
35133
- const rows = await this.db.update(inboxMessages).set({ status: "queued", updatedAt: now }).where(and(inArray(inboxMessages.id, msgIds), eq(inboxMessages.status, "injected"))).returning();
35134
- for (const row of rows) {
35135
- this.emitEvent("message.requeued", {
35136
- id: row.id,
35137
- fromId: row.fromId,
35138
- toId: row.toId,
35139
- timestamp: now
35140
- });
35141
- }
35142
- return rows.length;
35143
- }
35144
- async inbox(toId, limit, offset) {
35145
- return this.list({
35146
- where: { col: inboxMessages.toId, op: "eq", value: toId },
35147
- orderBy: [{ col: inboxMessages.createdAt, dir: "desc" }],
35148
- ...limit !== undefined ? { limit } : {},
35149
- ...offset !== undefined ? { offset } : {}
35150
- });
35151
- }
35152
- async countPending(toId) {
35153
- return this.count({
35154
- and: [
35155
- { col: inboxMessages.toId, op: "eq", value: toId },
35156
- { col: inboxMessages.status, op: "eq", value: "queued" }
35157
- ]
35158
- });
35159
- }
35160
- async getById(id) {
35161
- return this.findBy(inboxMessages.id, id);
35162
- }
35163
- emitEvent(event, ...args) {
35164
- if (!this.events)
35165
- return;
35166
- try {
35167
- const pending = this.events.emit(event, ...args);
35168
- if (isPromiseLike(pending)) {
35169
- Promise.resolve(pending).catch(() => {});
35170
- }
35171
- } catch {}
35172
- }
35173
- };
35174
- });
35175
-
35176
34980
  // ../../node_modules/.bun/@gobing-ai+ts-db@0.5.32+3ceefb3da453b19a/node_modules/@gobing-ai/ts-db/dist/queue-job-dao.js
35177
34981
  function assertValidTimeoutMs(value) {
35178
34982
  if (value === undefined || value === null)
@@ -35398,8 +35202,8 @@ var init_queue_job_dao = __esm(() => {
35398
35202
 
35399
35203
  // ../../node_modules/.bun/@gobing-ai+ts-db@0.5.32+3ceefb3da453b19a/node_modules/@gobing-ai/ts-db/dist/index.js
35400
35204
  var init_dist4 = __esm(() => {
35205
+ init_base_dao();
35401
35206
  init_entity_dao();
35402
- init_inbox_message_dao();
35403
35207
  init_queue_job_dao();
35404
35208
  });
35405
35209
 
@@ -48413,16 +48217,34 @@ class RunSessionDao {
48413
48217
  var init_retro_correlation = () => {};
48414
48218
 
48415
48219
  // ../../packages/domain/src/dao/system-event-dao.ts
48220
+ function systemEventInsertSql(input) {
48221
+ return {
48222
+ sql: `INSERT INTO system_events (${SYSTEM_EVENT_COLUMNS})
48223
+ SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8,
48224
+ CASE WHEN ?9 IS NULL THEN COALESCE(MAX(sequence), 0) + 1 ELSE ?9 END
48225
+ FROM system_events`,
48226
+ params: [
48227
+ input.id,
48228
+ input.event_name,
48229
+ input.occurred_at,
48230
+ input.actor ?? null,
48231
+ input.payload_json ?? null,
48232
+ input.run_id ?? null,
48233
+ input.entity_kind ?? null,
48234
+ input.entity_id ?? null,
48235
+ input.sequence ?? null
48236
+ ]
48237
+ };
48238
+ }
48239
+
48416
48240
  class SystemEventDao {
48417
48241
  db;
48418
48242
  constructor(db) {
48419
48243
  this.db = db;
48420
48244
  }
48421
48245
  async insert(input) {
48422
- await this.db.run(`INSERT INTO system_events (${SYSTEM_EVENT_COLUMNS})
48423
- SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8,
48424
- CASE WHEN ?9 IS NULL THEN COALESCE(MAX(sequence), 0) + 1 ELSE ?9 END
48425
- FROM system_events`, input.id, input.event_name, input.occurred_at, input.actor ?? null, input.payload_json ?? null, input.run_id ?? null, input.entity_kind ?? null, input.entity_id ?? null, input.sequence ?? null);
48246
+ const statement = systemEventInsertSql(input);
48247
+ await this.db.run(statement.sql, ...statement.params);
48426
48248
  }
48427
48249
  async pruneQuotas(quotas, prefix) {
48428
48250
  try {
@@ -50199,6 +50021,228 @@ class ActionRunDao {
50199
50021
  }
50200
50022
  }
50201
50023
  }
50024
+
50025
+ // ../../packages/domain/src/dao/inbox-dao.ts
50026
+ function toMessage(row) {
50027
+ return {
50028
+ id: row.id,
50029
+ fromId: row.from_id,
50030
+ toId: row.to_id,
50031
+ body: row.body,
50032
+ kind: row.kind === "request" ? "request" : "chat",
50033
+ status: row.status,
50034
+ claimedAt: row.claimed_at,
50035
+ attempts: row.attempts,
50036
+ error: row.error,
50037
+ deliveredAt: row.delivered_at,
50038
+ readAt: row.read_at,
50039
+ inReplyTo: row.in_reply_to,
50040
+ threadId: row.thread_id,
50041
+ requestKey: row.request_key,
50042
+ taskId: row.task_id,
50043
+ parentRunId: row.parent_run_id,
50044
+ createdAt: row.created_at,
50045
+ updatedAt: row.updated_at
50046
+ };
50047
+ }
50048
+ function placeholders(values, start) {
50049
+ return values.map((_, index) => `?${start + index}`).join(", ");
50050
+ }
50051
+ var COLUMNS = `id, from_id, to_id, body, kind, status, claimed_at, attempts, error, delivered_at,
50052
+ read_at, in_reply_to, thread_id, request_key, task_id, parent_run_id, created_at, updated_at`, transactionLocks, InboxDao;
50053
+ var init_inbox_dao = __esm(() => {
50054
+ init_dist4();
50055
+ init_system_event_dao();
50056
+ transactionLocks = new WeakMap;
50057
+ InboxDao = class InboxDao extends BaseDao {
50058
+ constructor(adapter) {
50059
+ super(adapter);
50060
+ }
50061
+ async withTransaction(fn) {
50062
+ const previous = transactionLocks.get(this.adapter) ?? Promise.resolve();
50063
+ const run = previous.then(async () => {
50064
+ await this.adapter.exec("BEGIN IMMEDIATE");
50065
+ try {
50066
+ const result = await fn();
50067
+ await this.adapter.exec("COMMIT");
50068
+ return result;
50069
+ } catch (error) {
50070
+ await this.adapter.exec("ROLLBACK");
50071
+ throw error;
50072
+ }
50073
+ });
50074
+ transactionLocks.set(this.adapter, run.then(() => {
50075
+ return;
50076
+ }, () => {
50077
+ return;
50078
+ }));
50079
+ return run;
50080
+ }
50081
+ async writeEvents(toEvent, changed) {
50082
+ for (const row of changed) {
50083
+ const statement = systemEventInsertSql(toEvent(row));
50084
+ await this.adapter.run(statement.sql, ...statement.params);
50085
+ }
50086
+ }
50087
+ async insert(input, toEvent) {
50088
+ return this.withTransaction(async () => {
50089
+ const [row] = await this.adapter.queryAll(`INSERT INTO inbox_messages (${COLUMNS})
50090
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL, 0, NULL, NULL, NULL, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
50091
+ RETURNING ${COLUMNS}`, input.id, input.fromId, input.toId, input.body, input.kind, input.status, input.inReplyTo, input.threadId, input.requestKey, input.taskId, input.parentRunId, input.createdAt, input.updatedAt);
50092
+ if (row === undefined)
50093
+ throw new Error(`inbox insert returned no row for ${input.id}`);
50094
+ const message = toMessage(row);
50095
+ await this.writeEvents(toEvent, [message]);
50096
+ return message;
50097
+ });
50098
+ }
50099
+ async findByRequestKey(requestKey) {
50100
+ const [row] = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages WHERE request_key = ?1`, requestKey);
50101
+ return row === undefined ? null : toMessage(row);
50102
+ }
50103
+ async get(id) {
50104
+ const [row] = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages WHERE id = ?1`, id);
50105
+ return row === undefined ? null : toMessage(row);
50106
+ }
50107
+ async claim(member, limit, now, toEvent) {
50108
+ return this.withTransaction(async () => {
50109
+ const rows = await this.adapter.queryAll(`UPDATE inbox_messages
50110
+ SET status = 'claimed', claimed_at = ?2, attempts = attempts + 1, updated_at = ?2
50111
+ WHERE id IN (
50112
+ SELECT id FROM inbox_messages
50113
+ WHERE to_id = ?1 AND status = 'queued'
50114
+ ORDER BY created_at ASC, rowid ASC
50115
+ LIMIT ?3
50116
+ )
50117
+ RETURNING ${COLUMNS}`, member, now, Math.max(0, limit));
50118
+ const changed = rows.map(toMessage);
50119
+ await this.writeEvents(toEvent, changed);
50120
+ return changed;
50121
+ });
50122
+ }
50123
+ async settle(ids, to, now, toEvent, error = null) {
50124
+ if (ids.length === 0)
50125
+ return [];
50126
+ return this.withTransaction(async () => {
50127
+ const rows = await this.adapter.queryAll(`UPDATE inbox_messages
50128
+ SET status = ?1, error = ?2,
50129
+ delivered_at = CASE WHEN ?1 = 'delivered' THEN ?3 ELSE NULL END,
50130
+ updated_at = ?3
50131
+ WHERE id IN (${placeholders(ids, 4)}) AND status = 'claimed'
50132
+ RETURNING ${COLUMNS}`, to, error, now, ...ids);
50133
+ const changed = rows.map(toMessage);
50134
+ await this.writeEvents(toEvent, changed);
50135
+ return changed;
50136
+ });
50137
+ }
50138
+ async release(ids, now, toEvent) {
50139
+ if (ids.length === 0)
50140
+ return [];
50141
+ return this.withTransaction(async () => {
50142
+ const rows = await this.adapter.queryAll(`UPDATE inbox_messages
50143
+ SET status = 'queued', claimed_at = NULL, updated_at = ?1
50144
+ WHERE id IN (${placeholders(ids, 2)}) AND status = 'claimed'
50145
+ RETURNING ${COLUMNS}`, now, ...ids);
50146
+ const changed = rows.map(toMessage);
50147
+ await this.writeEvents(toEvent, changed);
50148
+ return changed;
50149
+ });
50150
+ }
50151
+ async markRead(id, reader, now, toEvent) {
50152
+ return this.withTransaction(async () => {
50153
+ const [stamped] = await this.adapter.queryAll(`UPDATE inbox_messages SET read_at = ?3, updated_at = ?3
50154
+ WHERE id = ?1 AND to_id = ?2 AND read_at IS NULL
50155
+ RETURNING ${COLUMNS}`, id, reader, now);
50156
+ if (stamped !== undefined) {
50157
+ const message = toMessage(stamped);
50158
+ await this.writeEvents(toEvent, [message]);
50159
+ return { message, transitioned: true };
50160
+ }
50161
+ const [existing] = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages WHERE id = ?1 AND to_id = ?2`, id, reader);
50162
+ return existing === undefined ? null : { message: toMessage(existing), transitioned: false };
50163
+ });
50164
+ }
50165
+ async listExpiredClaims(cutoff) {
50166
+ const rows = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages
50167
+ WHERE status = 'claimed' AND claimed_at IS NOT NULL AND claimed_at < ?1
50168
+ ORDER BY claimed_at ASC, rowid ASC`, cutoff);
50169
+ return rows.map(toMessage);
50170
+ }
50171
+ async list(member, direction, limit, before) {
50172
+ const scope = direction === "in" ? "to_id = ?1" : direction === "out" ? "from_id = ?1" : "(to_id = ?1 OR from_id = ?1)";
50173
+ const cursor = before === undefined ? "" : "AND (created_at < ?3 OR (created_at = ?3 AND id < ?4))";
50174
+ const params = [member, Math.max(0, limit) + 1];
50175
+ if (before !== undefined)
50176
+ params.push(before.createdAt, before.id);
50177
+ const rows = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages
50178
+ WHERE ${scope} ${cursor}
50179
+ ORDER BY created_at DESC, id DESC
50180
+ LIMIT ?2`, ...params);
50181
+ return rows.map(toMessage);
50182
+ }
50183
+ async thread(threadId) {
50184
+ const rows = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages
50185
+ WHERE thread_id = ?1 OR id = ?1
50186
+ ORDER BY created_at ASC, rowid ASC`, threadId);
50187
+ return rows.map(toMessage);
50188
+ }
50189
+ async requests(toIds) {
50190
+ if (toIds !== undefined && toIds.length === 0)
50191
+ return [];
50192
+ const scope = toIds === undefined ? `m.kind = 'request'` : `m.kind = 'request' AND m.to_id IN (${placeholders(toIds, 1)})`;
50193
+ const rows = await this.adapter.queryAll(`SELECT ${COLUMNS.split(",").map((column) => `m.${column.trim()}`).join(", ")},
50194
+ r.run_id AS run_id, r.status AS run_status, r.outcome AS run_outcome,
50195
+ r.task_id AS run_task_id, r.artifact_refs_json AS run_artifact_refs_json
50196
+ FROM inbox_messages m
50197
+ LEFT JOIN coordination_runs r ON r.run_id = (
50198
+ SELECT c.run_id FROM coordination_runs c, json_each(c.message_ids_json) j
50199
+ WHERE j.value = m.id
50200
+ ORDER BY c.started_at DESC
50201
+ LIMIT 1
50202
+ )
50203
+ WHERE ${scope}
50204
+ ORDER BY m.created_at DESC, m.rowid DESC`, ...toIds ?? []);
50205
+ return rows.map((row) => ({
50206
+ ...toMessage(row),
50207
+ runId: row.run_id ?? null,
50208
+ runStatus: row.run_status ?? null,
50209
+ runOutcome: row.run_outcome ?? null,
50210
+ runTaskId: row.run_task_id ?? null,
50211
+ runArtifactRefsJson: row.run_artifact_refs_json ?? null
50212
+ }));
50213
+ }
50214
+ async countQueued(member) {
50215
+ const [row] = await this.adapter.queryAll(`SELECT COUNT(*) AS n FROM inbox_messages WHERE to_id = ?1 AND status = 'queued'`, member);
50216
+ return row?.n ?? 0;
50217
+ }
50218
+ async distinctRecipients() {
50219
+ return this.adapter.queryAll(`SELECT to_id AS id,
50220
+ SUM(CASE WHEN status = 'queued' THEN 1 ELSE 0 END) AS queued,
50221
+ SUM(CASE WHEN read_at IS NULL THEN 1 ELSE 0 END) AS unread
50222
+ FROM inbox_messages
50223
+ GROUP BY to_id
50224
+ ORDER BY to_id ASC`);
50225
+ }
50226
+ async attemptsForTask(taskId) {
50227
+ const rows = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages
50228
+ WHERE task_id = ?1
50229
+ ORDER BY created_at DESC, rowid DESC`, taskId);
50230
+ return { count: rows.length, latest: rows[0] === undefined ? null : toMessage(rows[0]) };
50231
+ }
50232
+ async listByTask(taskId) {
50233
+ const rows = await this.adapter.queryAll(`SELECT ${COLUMNS} FROM inbox_messages
50234
+ WHERE task_id = ?1
50235
+ ORDER BY created_at DESC, rowid DESC`, taskId);
50236
+ return rows.map(toMessage);
50237
+ }
50238
+ };
50239
+ });
50240
+
50241
+ // ../../packages/domain/src/dao/addressed-spec-ids.ts
50242
+ var init_addressed_spec_ids = __esm(() => {
50243
+ init_inbox_dao();
50244
+ });
50245
+
50202
50246
  // ../../packages/domain/src/dao/agent-executor-update-dao.ts
50203
50247
  class AgentExecutorUpdateDao {
50204
50248
  db;
@@ -51300,113 +51344,6 @@ var init_decision_log_dao = __esm(() => {
51300
51344
  ];
51301
51345
  });
51302
51346
 
51303
- // ../../packages/domain/src/dao/inbox-recent-dao.ts
51304
- class InboxRecentDao {
51305
- db;
51306
- constructor(db) {
51307
- this.db = db;
51308
- }
51309
- async listRecent(limit = 50) {
51310
- const capped = Math.max(1, Math.min(limit, 500));
51311
- try {
51312
- return await this.db.queryAll(`SELECT id, from_id, to_id, body, status, created_at, in_reply_to
51313
- FROM inbox_messages
51314
- ORDER BY created_at DESC
51315
- LIMIT ?1`, capped);
51316
- } catch (error) {
51317
- if (error instanceof Error && error.message.includes("no such table: inbox_messages")) {
51318
- return [];
51319
- }
51320
- throw error;
51321
- }
51322
- }
51323
- async countReplies(ids) {
51324
- const result = new Map;
51325
- if (ids.length === 0)
51326
- return result;
51327
- const placeholders = ids.map((_, i) => `?${i + 1}`).join(", ");
51328
- try {
51329
- const rows = await this.db.queryAll(`SELECT in_reply_to, COUNT(*) AS n
51330
- FROM inbox_messages
51331
- WHERE in_reply_to IS NOT NULL AND in_reply_to IN (${placeholders})
51332
- GROUP BY in_reply_to`, ...ids);
51333
- for (const row of rows) {
51334
- result.set(row.in_reply_to, row.n);
51335
- }
51336
- } catch (error) {
51337
- if (error instanceof Error && error.message.includes("no such table: inbox_messages")) {
51338
- return result;
51339
- }
51340
- throw error;
51341
- }
51342
- return result;
51343
- }
51344
- async markRead(id, readerId, nowMs = Date.now()) {
51345
- try {
51346
- const [stamped] = await this.db.queryAll(`UPDATE inbox_messages SET read_at = ?3
51347
- WHERE id = ?1 AND to_id = ?2 AND read_at IS NULL
51348
- RETURNING read_at, from_id, in_reply_to`, id, readerId, nowMs);
51349
- if (stamped)
51350
- return toReceipt(stamped, true);
51351
- const [existing] = await this.db.queryAll("SELECT read_at, from_id, in_reply_to FROM inbox_messages WHERE id = ?1 AND to_id = ?2", id, readerId);
51352
- return existing?.read_at == null ? null : toReceipt(existing, false);
51353
- } catch (error) {
51354
- if (error instanceof Error && error.message.includes("no such table: inbox_messages")) {
51355
- return null;
51356
- }
51357
- throw error;
51358
- }
51359
- }
51360
- }
51361
- var toReceipt = (row, transitioned) => ({
51362
- readAt: row.read_at ?? 0,
51363
- transitioned,
51364
- fromId: row.from_id,
51365
- inReplyTo: row.in_reply_to
51366
- });
51367
-
51368
- // ../../packages/domain/src/dao/inbox-unfinished-dao.ts
51369
- class InboxUnfinishedDao {
51370
- db;
51371
- constructor(db) {
51372
- this.db = db;
51373
- }
51374
- async listUnfinished(toId) {
51375
- try {
51376
- if (toId !== undefined) {
51377
- return await this.db.queryAll(`SELECT id, to_id, status, inject_attempts, inject_error, created_at, request_key
51378
- FROM inbox_messages
51379
- WHERE to_id = ?1
51380
- ORDER BY created_at DESC`, toId) ?? [];
51381
- }
51382
- return await this.db.queryAll(`SELECT id, to_id, status, inject_attempts, inject_error, created_at, request_key
51383
- FROM inbox_messages
51384
- ORDER BY created_at DESC`) ?? [];
51385
- } catch (error) {
51386
- if (error instanceof Error && error.message.includes("no such table: inbox_messages")) {
51387
- return [];
51388
- }
51389
- throw error;
51390
- }
51391
- }
51392
- async listByRequestKeyPrefix(prefix) {
51393
- if (prefix === "") {
51394
- throw new Error("listByRequestKeyPrefix requires a non-empty prefix");
51395
- }
51396
- try {
51397
- return await this.db.queryAll(`SELECT id, to_id, status, inject_attempts, inject_error, created_at, request_key
51398
- FROM inbox_messages
51399
- WHERE request_key LIKE ?1 ESCAPE '\\'
51400
- ORDER BY created_at DESC`, `${prefix.replace(/[\\%_]/g, "\\$&")}%`) ?? [];
51401
- } catch (error) {
51402
- if (error instanceof Error && error.message.includes("no such table: inbox_messages")) {
51403
- return [];
51404
- }
51405
- throw error;
51406
- }
51407
- }
51408
- }
51409
-
51410
51347
  // ../../packages/domain/src/dao/member-lifecycle.ts
51411
51348
  function parseObservation(payloadJson) {
51412
51349
  if (payloadJson === null)
@@ -52264,10 +52201,11 @@ var init_workspace_dao = __esm(() => {
52264
52201
 
52265
52202
  // ../../packages/domain/src/dao/index.ts
52266
52203
  var init_dao = __esm(() => {
52267
- init_dist4();
52204
+ init_addressed_spec_ids();
52268
52205
  init_artifact_dao();
52269
52206
  init_database_inspector();
52270
52207
  init_decision_log_dao();
52208
+ init_inbox_dao();
52271
52209
  init_member_lifecycle();
52272
52210
  init_member_session();
52273
52211
  init_phase_run_dao();
@@ -57721,17 +57659,17 @@ function parentDir(filePath) {
57721
57659
  const idx = filePath.lastIndexOf(sep);
57722
57660
  return idx <= 0 ? "." : filePath.slice(0, idx);
57723
57661
  }
57724
- function isPromiseLike2(value) {
57662
+ function isPromiseLike(value) {
57725
57663
  return value !== null && typeof value === "object" && "then" in value;
57726
57664
  }
57727
57665
  function attachFileObserver(lifecycleBus, filePath, writer) {
57728
57666
  const logger = getLogger2("event-bus.file-observer");
57729
57667
  const ensureResult = writer.ensureDir(parentDir(filePath));
57730
- let ready = !isPromiseLike2(ensureResult);
57731
- let pending = isPromiseLike2(ensureResult);
57668
+ let ready = !isPromiseLike(ensureResult);
57669
+ let pending = isPromiseLike(ensureResult);
57732
57670
  let setupFailed = false;
57733
57671
  let sequence = 0;
57734
- let writeChain = isPromiseLike2(ensureResult) ? Promise.resolve(ensureResult).then(() => {
57672
+ let writeChain = isPromiseLike(ensureResult) ? Promise.resolve(ensureResult).then(() => {
57735
57673
  ready = true;
57736
57674
  }).catch((error) => {
57737
57675
  setupFailed = true;
@@ -57760,7 +57698,7 @@ function attachFileObserver(lifecycleBus, filePath, writer) {
57760
57698
  return;
57761
57699
  if (ready && !pending) {
57762
57700
  const result = writer.appendFile(filePath, content);
57763
- if (isPromiseLike2(result)) {
57701
+ if (isPromiseLike(result)) {
57764
57702
  setPending(Promise.resolve(result));
57765
57703
  }
57766
57704
  return;
@@ -65480,6 +65418,41 @@ var init_dist10 = __esm(() => {
65480
65418
  init_types3();
65481
65419
  });
65482
65420
 
65421
+ // ../../packages/domain/src/schema/inbox-messages.ts
65422
+ var inboxMessagesTable, inboxMessages3, INBOX_MESSAGES_TABLE_SQL;
65423
+ var init_inbox_messages2 = __esm(() => {
65424
+ init_schema();
65425
+ inboxMessagesTable = defineTable("inbox_messages", {
65426
+ id: text("id").primaryKey(),
65427
+ fromId: text("from_id").notNull(),
65428
+ toId: text("to_id").notNull(),
65429
+ body: text("body").notNull(),
65430
+ kind: text("kind").notNull().default("chat"),
65431
+ status: text("status").notNull().default("queued"),
65432
+ claimedAt: integer2("claimed_at"),
65433
+ attempts: integer2("attempts").notNull().default(0),
65434
+ error: text("error"),
65435
+ deliveredAt: integer2("delivered_at"),
65436
+ readAt: integer2("read_at"),
65437
+ inReplyTo: text("in_reply_to"),
65438
+ threadId: text("thread_id"),
65439
+ requestKey: text("request_key"),
65440
+ taskId: text("task_id"),
65441
+ parentRunId: text("parent_run_id"),
65442
+ createdAt: integer2("created_at").notNull(),
65443
+ updatedAt: integer2("updated_at").notNull()
65444
+ });
65445
+ inboxMessages3 = inboxMessagesTable.table;
65446
+ INBOX_MESSAGES_TABLE_SQL = [
65447
+ inboxMessagesTable.createTableSql,
65448
+ "CREATE INDEX IF NOT EXISTS idx_inbox_messages_to_status ON inbox_messages (to_id, status)",
65449
+ "CREATE INDEX IF NOT EXISTS idx_inbox_messages_from_created ON inbox_messages (from_id, created_at)",
65450
+ "CREATE INDEX IF NOT EXISTS idx_inbox_messages_thread ON inbox_messages (thread_id)",
65451
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_messages_request_key ON inbox_messages (request_key) WHERE request_key IS NOT NULL"
65452
+ ].map((sql) => `${sql};`).join(`
65453
+ `);
65454
+ });
65455
+
65483
65456
  // ../../packages/domain/src/schema/planning.ts
65484
65457
  var planningEventsTable, planningEvents, taskRunLinksTable, taskRunLinks, PLANNING_SCHEMA_SQL;
65485
65458
  var init_planning = __esm(() => {
@@ -65518,6 +65491,7 @@ var init_schema3 = __esm(() => {
65518
65491
  init_workflow_states();
65519
65492
  init_workspaces();
65520
65493
  init_artifacts();
65494
+ init_inbox_messages2();
65521
65495
  init_phase_runs();
65522
65496
  init_planning();
65523
65497
  init_transition_runs();
@@ -65698,7 +65672,7 @@ ALTER TABLE inbox_messages ADD COLUMN request_key TEXT;
65698
65672
  CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_messages_request_key ON inbox_messages (request_key) WHERE request_key IS NOT NULL;
65699
65673
  `, INBOX_MESSAGES_READ_AT_SCHEMA_SQL = `
65700
65674
  ALTER TABLE inbox_messages ADD COLUMN read_at INTEGER;
65701
- `, QUEUE_JOBS_SCHEMA_SQL = `
65675
+ `, INBOX_MESSAGES_REBUILD_SQL, QUEUE_JOBS_SCHEMA_SQL = `
65702
65676
  CREATE TABLE IF NOT EXISTS queue_jobs (
65703
65677
  id TEXT PRIMARY KEY,
65704
65678
  type TEXT NOT NULL,
@@ -66457,6 +66431,10 @@ var init_migrations = __esm(() => {
66457
66431
  init_dist5();
66458
66432
  init_importer_schema_version();
66459
66433
  init_schema3();
66434
+ INBOX_MESSAGES_REBUILD_SQL = `
66435
+ DROP TABLE IF EXISTS inbox_messages;
66436
+ ${INBOX_MESSAGES_TABLE_SQL}
66437
+ `;
66460
66438
  CLI_SCHEMA_SQL = `
66461
66439
  ${AGENT_EXECUTOR_UPDATES_SCHEMA_SQL}
66462
66440
 
@@ -66680,6 +66658,10 @@ ${HISTORY_TASK_SESSION_SCHEMA_SQL}
66680
66658
  id: "0053_spur_cli_inbox_messages_read_at",
66681
66659
  sql: INBOX_MESSAGES_READ_AT_SCHEMA_SQL,
66682
66660
  addColumnIfMissing: { table: "inbox_messages", column: "read_at" }
66661
+ },
66662
+ {
66663
+ id: "0054_spur_cli_inbox_messages_rebuild",
66664
+ sql: INBOX_MESSAGES_REBUILD_SQL
66683
66665
  }
66684
66666
  ];
66685
66667
  });
@@ -84619,6 +84601,10 @@ var init_event_names = __esm(() => {
84619
84601
  baseEvent("message.sent", "message", "message"),
84620
84602
  baseEvent("message.replied", "message", "message"),
84621
84603
  baseEvent("message.read", "message", "message"),
84604
+ baseEvent("message.claimed", "message", "message"),
84605
+ baseEvent("message.delivered", "message", "message"),
84606
+ baseEvent("message.failed", "message", "message"),
84607
+ baseEvent("message.requeued", "message", "message"),
84622
84608
  baseEvent("process.spawned", "process", "process"),
84623
84609
  baseEvent("process.exited", "process", "process"),
84624
84610
  baseEvent("process.stopped", "process", "process"),
@@ -84957,6 +84943,66 @@ var init_event_names = __esm(() => {
84957
84943
  },
84958
84944
  outcome: unsupported
84959
84945
  },
84946
+ "message.claimed": {
84947
+ description: "A member claimed a queued inbox message; the delivery budget was charged.",
84948
+ fields: [
84949
+ field("msgId", "Message"),
84950
+ field("fromId", "From"),
84951
+ field("toId", "Member"),
84952
+ field("threadId", "Thread"),
84953
+ field("status", "Status")
84954
+ ],
84955
+ summary: ({ data }) => {
84956
+ const to = s2(data, "toId");
84957
+ return to !== undefined ? `[message] ${to} claimed` : "[message] claimed";
84958
+ },
84959
+ outcome: unsupported
84960
+ },
84961
+ "message.delivered": {
84962
+ description: "A claimed message was delivered to its member.",
84963
+ fields: [
84964
+ field("msgId", "Message"),
84965
+ field("fromId", "From"),
84966
+ field("toId", "Member"),
84967
+ field("threadId", "Thread"),
84968
+ field("status", "Status")
84969
+ ],
84970
+ summary: ({ data }) => {
84971
+ const to = s2(data, "toId");
84972
+ return to !== undefined ? `[message] ${to} delivered` : "[message] delivered";
84973
+ },
84974
+ outcome: unsupported
84975
+ },
84976
+ "message.failed": {
84977
+ description: "A message could not be delivered \u2014 the attempt budget was spent or the run errored.",
84978
+ fields: [
84979
+ field("msgId", "Message"),
84980
+ field("fromId", "From"),
84981
+ field("toId", "Member"),
84982
+ field("threadId", "Thread"),
84983
+ field("status", "Status")
84984
+ ],
84985
+ summary: ({ data }) => {
84986
+ const to = s2(data, "toId");
84987
+ return to !== undefined ? `[message] ${to} delivery failed` : "[message] failed";
84988
+ },
84989
+ outcome: unsupported
84990
+ },
84991
+ "message.requeued": {
84992
+ description: "A claim expired or was released, returning the message to the member queue.",
84993
+ fields: [
84994
+ field("msgId", "Message"),
84995
+ field("fromId", "From"),
84996
+ field("toId", "Member"),
84997
+ field("threadId", "Thread"),
84998
+ field("status", "Status")
84999
+ ],
85000
+ summary: ({ data }) => {
85001
+ const to = s2(data, "toId");
85002
+ return to !== undefined ? `[message] ${to} requeued` : "[message] requeued";
85003
+ },
85004
+ outcome: unsupported
85005
+ },
84960
85006
  "process.spawned": {
84961
85007
  description: "A supervised process was spawned with a new pid.",
84962
85008
  fields: [field("label", "Label"), field("pid", "PID"), field("agentId", "Agent")],
@@ -90820,8 +90866,482 @@ var init_fleet_service = __esm(() => {
90820
90866
  init_project_registry();
90821
90867
  });
90822
90868
 
90869
+ // ../../packages/app/src/services/system-event-retention.ts
90870
+ function resolveRetentionQuotas(config = {}, prefixes = SYSTEM_EVENT_PREFIXES) {
90871
+ const defaultQuota = config.default ?? DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA;
90872
+ const overrides = config.prefixes ?? {};
90873
+ return prefixes.map((prefix) => ({
90874
+ prefix,
90875
+ quota: overrides[prefix] ?? defaultQuota
90876
+ }));
90877
+ }
90878
+ var DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA = 1e4;
90879
+ var init_system_event_retention = __esm(() => {
90880
+ init_event_names();
90881
+ });
90882
+
90883
+ // ../../packages/app/src/services/system-event-tap.ts
90884
+ function registerSystemEventTap(bus, dao, logger, options = {}) {
90885
+ const handlers = new Map;
90886
+ const inFlight = new Set;
90887
+ const diagnosticEnabled = options.diagnosticEnabled === true;
90888
+ const quotas = resolveRetentionQuotas(options.retention);
90889
+ for (const entry of SYSTEM_EVENT_CATALOG) {
90890
+ if (entry.tier === "diagnostic" && !diagnosticEnabled)
90891
+ continue;
90892
+ if (entry.prefix === "message")
90893
+ continue;
90894
+ const handler = (event) => {
90895
+ const actor = extractSystemEventActor(event);
90896
+ const p = persist(dao, entry.prefix, {
90897
+ id: createId("sev"),
90898
+ event_name: entry.name,
90899
+ occurred_at: new Date().toISOString(),
90900
+ actor,
90901
+ payload_json: safeStringify(buildSystemEventEnvelope(entry, event, options.projectContext ?? systemEventProjectContext(""), options.secretValues, actor)),
90902
+ ...extractSystemEventCorrelation(event)
90903
+ }, quotas, logger);
90904
+ inFlight.add(p);
90905
+ p.finally(() => inFlight.delete(p));
90906
+ };
90907
+ handlers.set(entry.name, handler);
90908
+ bus.on(entry.name, handler);
90909
+ }
90910
+ return {
90911
+ unsubscribe: () => {
90912
+ for (const [name, handler] of handlers) {
90913
+ bus.off(name, handler);
90914
+ }
90915
+ handlers.clear();
90916
+ },
90917
+ flush: async () => {
90918
+ while (inFlight.size > 0) {
90919
+ await Promise.allSettled(inFlight);
90920
+ }
90921
+ }
90922
+ };
90923
+ }
90924
+ async function persist(dao, prefix, input, quotas, logger) {
90925
+ try {
90926
+ await dao.insert(input);
90927
+ await dao.pruneQuotas(quotas, prefix);
90928
+ } catch (error) {
90929
+ logger.warn("system_events tap: persist failed", {
90930
+ name: input.event_name,
90931
+ error: stringifyError(error)
90932
+ });
90933
+ }
90934
+ }
90935
+ function safeStringify(event) {
90936
+ try {
90937
+ return JSON.stringify(event ?? null);
90938
+ } catch {
90939
+ return null;
90940
+ }
90941
+ }
90942
+ function extractSystemEventActor(event) {
90943
+ if (event && typeof event === "object") {
90944
+ const obj = event;
90945
+ if (typeof obj.actor === "string" && obj.actor.length > 0)
90946
+ return obj.actor;
90947
+ if (typeof obj.agentId === "string" && obj.agentId.length > 0)
90948
+ return obj.agentId;
90949
+ if (typeof obj.memberId === "string" && obj.memberId.length > 0)
90950
+ return obj.memberId;
90951
+ }
90952
+ return null;
90953
+ }
90954
+ function extractSystemEventCorrelation(event) {
90955
+ const correlation = { run_id: null, entity_kind: null, entity_id: null, sequence: null };
90956
+ if (!event || typeof event !== "object")
90957
+ return correlation;
90958
+ const obj = event;
90959
+ const nested = obj.correlation && typeof obj.correlation === "object" ? obj.correlation : {};
90960
+ const runId = obj.runId ?? obj.run_id ?? nested.runId;
90961
+ if (typeof runId === "string" && runId.length > 0)
90962
+ correlation.run_id = runId;
90963
+ if (typeof obj.sequence === "number" && Number.isFinite(obj.sequence))
90964
+ correlation.sequence = obj.sequence;
90965
+ const entity = obj.entity;
90966
+ if (entity && typeof entity === "object") {
90967
+ const { kind, id } = entity;
90968
+ if (typeof kind === "string" && kind.length > 0)
90969
+ correlation.entity_kind = kind;
90970
+ if (typeof id === "string" && id.length > 0)
90971
+ correlation.entity_id = id;
90972
+ }
90973
+ if (typeof obj.entityKind === "string" && obj.entityKind.length > 0)
90974
+ correlation.entity_kind = obj.entityKind;
90975
+ if (typeof obj.entityId === "string" && obj.entityId.length > 0)
90976
+ correlation.entity_id = obj.entityId;
90977
+ return correlation;
90978
+ }
90979
+ function stringifyError(error) {
90980
+ if (error instanceof Error)
90981
+ return `${error.name}: ${error.message}`;
90982
+ return String(error);
90983
+ }
90984
+ var init_system_event_tap = __esm(() => {
90985
+ init_src2();
90986
+ init_event_names();
90987
+ init_system_event_envelope();
90988
+ init_system_event_retention();
90989
+ });
90990
+
90991
+ // ../../packages/app/src/services/system-event-catch-all.ts
90992
+ function genericSystemEventCatalogEntry(name) {
90993
+ return {
90994
+ name,
90995
+ prefix: name.replace(/\..*/, ""),
90996
+ source: "bus",
90997
+ tier: "default",
90998
+ persisted: true,
90999
+ streamed: true,
91000
+ payloadPolicy: "metadata-only",
91001
+ renderer: GENERIC_SYSTEM_EVENT_RENDERER,
91002
+ producerPackage: "spur",
91003
+ subsystem: "unknown",
91004
+ severity: "info",
91005
+ description: "Uncataloged system event captured by the generic fallback; pending catalog promotion.",
91006
+ metadataFields: [],
91007
+ remediationKind: "none"
91008
+ };
91009
+ }
91010
+ function ensureRetentionQuotaForPrefix(quotas, prefix, fallbackQuota) {
91011
+ return quotas.some((quota) => quota.prefix === prefix) ? quotas : [...quotas, { prefix, quota: fallbackQuota }];
91012
+ }
91013
+ function createSystemEventCatchAllSink(options) {
91014
+ const { dao, logger } = options;
91015
+ const quotas = resolveRetentionQuotas(options.retention);
91016
+ const fallbackQuota = options.retention?.default ?? DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA;
91017
+ const projectContext = options.projectContext ?? systemEventProjectContext("");
91018
+ const secretValues = options.secretValues ?? [];
91019
+ const driftWarned = new Set;
91020
+ return async (name, payload) => {
91021
+ const entry = genericSystemEventCatalogEntry(name);
91022
+ const actor = extractSystemEventActor(payload);
91023
+ try {
91024
+ await dao.insert({
91025
+ id: createId("sev"),
91026
+ event_name: entry.name,
91027
+ occurred_at: new Date().toISOString(),
91028
+ actor,
91029
+ payload_json: safeStringify(buildSystemEventEnvelope(entry, payload, projectContext, secretValues, actor)),
91030
+ ...extractSystemEventCorrelation(payload)
91031
+ });
91032
+ await dao.pruneQuotas(ensureRetentionQuotaForPrefix(quotas, entry.prefix, fallbackQuota), entry.prefix);
91033
+ } catch (error) {
91034
+ logger.warn("system_events catch-all: persist failed", {
91035
+ event: name,
91036
+ error: error instanceof Error ? `${error.name}: ${error.message}` : String(error)
91037
+ });
91038
+ return;
91039
+ }
91040
+ if (!driftWarned.has(name)) {
91041
+ driftWarned.add(name);
91042
+ logger.warn(SYSTEM_EVENT_UNCATALOGED_WARN_EVENT, { event: name });
91043
+ }
91044
+ };
91045
+ }
91046
+ function installSystemEventCatchAll(bus, sink) {
91047
+ const holder = bus;
91048
+ const current = holder.emit;
91049
+ if (current[CATCH_ALL_MARKER] === true) {
91050
+ return { installed: false, flush: async () => {} };
91051
+ }
91052
+ const underlying = current.bind(bus);
91053
+ const inFlight = new Set;
91054
+ const wrapped = (event, ...args) => {
91055
+ const result = underlying(event, ...args);
91056
+ if (systemEventCatalogEntry(event) === undefined) {
91057
+ const pending = Promise.resolve().then(() => sink(event, args[0]));
91058
+ inFlight.add(pending);
91059
+ pending.catch(() => {}).finally(() => {
91060
+ inFlight.delete(pending);
91061
+ });
91062
+ }
91063
+ return result;
91064
+ };
91065
+ Object.defineProperty(wrapped, CATCH_ALL_MARKER, { value: true });
91066
+ holder.emit = wrapped;
91067
+ return {
91068
+ installed: true,
91069
+ flush: async () => {
91070
+ while (inFlight.size > 0) {
91071
+ await Promise.allSettled([...inFlight]);
91072
+ }
91073
+ }
91074
+ };
91075
+ }
91076
+ var GENERIC_SYSTEM_EVENT_RENDERER = "generic", SYSTEM_EVENT_UNCATALOGED_WARN_EVENT = "system_events.uncataloged", CATCH_ALL_MARKER = "spur.systemEventCatchAll";
91077
+ var init_system_event_catch_all = __esm(() => {
91078
+ init_src2();
91079
+ init_event_names();
91080
+ init_system_event_envelope();
91081
+ init_system_event_retention();
91082
+ init_system_event_tap();
91083
+ });
91084
+
91085
+ // ../../packages/app/src/services/inbox-service.ts
91086
+ function isReservedInboxId(id) {
91087
+ return RESERVED_INBOX_IDS.includes(id);
91088
+ }
91089
+ function assertSameRequest(existing, input) {
91090
+ const sameRequest = existing.fromId === input.from && existing.toId === input.to && existing.body === input.body && existing.kind === (input.kind ?? "chat");
91091
+ if (!sameRequest)
91092
+ throw new RequestKeyConflictError2(input.requestKey ?? "");
91093
+ return existing;
91094
+ }
91095
+ function isRequestKeyCollision(error) {
91096
+ if (!(error instanceof Error))
91097
+ return false;
91098
+ const code = error.code;
91099
+ if (code === "SQLITE_CONSTRAINT_UNIQUE") {
91100
+ return error.message.includes("inbox_messages.request_key") || !error.message.includes("constraint failed");
91101
+ }
91102
+ return error.message.includes("UNIQUE constraint failed") && error.message.includes("inbox_messages.request_key");
91103
+ }
91104
+ function encodeCursor(row) {
91105
+ return Buffer.from(JSON.stringify({ c: row.createdAt, i: row.id })).toString("base64url");
91106
+ }
91107
+ function decodeCursor(cursor) {
91108
+ try {
91109
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
91110
+ if (typeof parsed.c !== "number" || typeof parsed.i !== "string")
91111
+ throw new Error("shape");
91112
+ return { createdAt: parsed.c, id: parsed.i };
91113
+ } catch {
91114
+ throw new Error("malformed list cursor");
91115
+ }
91116
+ }
91117
+
91118
+ class InboxService {
91119
+ deps;
91120
+ leaseMs;
91121
+ clock;
91122
+ constructor(deps) {
91123
+ this.deps = deps;
91124
+ this.leaseMs = deps.leaseMs ?? CLAIM_LEASE_MS;
91125
+ this.clock = deps.now ?? (() => Date.now());
91126
+ }
91127
+ async send(input) {
91128
+ if (input.from.trim() === "")
91129
+ throw new Error("inbox message requires a non-empty sender");
91130
+ if (input.to.trim() === "")
91131
+ throw new Error("inbox message requires a non-empty recipient");
91132
+ const db = await this.deps.getDb();
91133
+ const dao = new InboxDao(db);
91134
+ if (input.requestKey !== undefined) {
91135
+ const existing = await dao.findByRequestKey(input.requestKey);
91136
+ if (existing !== null)
91137
+ return { message: assertSameRequest(existing, input), replayed: true };
91138
+ }
91139
+ const now = this.clock();
91140
+ const id = `msg_${crypto.randomUUID()}`;
91141
+ let message;
91142
+ try {
91143
+ message = await dao.insert({
91144
+ id,
91145
+ fromId: input.from,
91146
+ toId: input.to,
91147
+ body: input.body,
91148
+ kind: input.kind ?? "chat",
91149
+ status: "queued",
91150
+ requestKey: input.requestKey ?? null,
91151
+ taskId: input.taskId ?? null,
91152
+ parentRunId: input.parentRunId ?? null,
91153
+ inReplyTo: null,
91154
+ threadId: id,
91155
+ createdAt: now,
91156
+ updatedAt: now
91157
+ }, this.eventFactory("message.sent"));
91158
+ } catch (error) {
91159
+ if (input.requestKey !== undefined && isRequestKeyCollision(error)) {
91160
+ const winner = await dao.findByRequestKey(input.requestKey);
91161
+ if (winner !== null)
91162
+ return { message: assertSameRequest(winner, input), replayed: true };
91163
+ }
91164
+ throw error;
91165
+ }
91166
+ await this.pruneEvents(db);
91167
+ return { message, replayed: false };
91168
+ }
91169
+ async reply(id, from, body) {
91170
+ const db = await this.deps.getDb();
91171
+ const dao = new InboxDao(db);
91172
+ const original = await dao.get(id);
91173
+ if (original === null)
91174
+ throw new MessageNotFoundError(`No message found with id "${id}"`);
91175
+ const now = this.clock();
91176
+ const replyId = `msg_${crypto.randomUUID()}`;
91177
+ const message = await dao.insert({
91178
+ id: replyId,
91179
+ fromId: from,
91180
+ toId: original.fromId,
91181
+ body,
91182
+ kind: original.kind,
91183
+ status: "queued",
91184
+ requestKey: null,
91185
+ taskId: original.taskId,
91186
+ parentRunId: original.parentRunId,
91187
+ inReplyTo: original.id,
91188
+ threadId: original.threadId,
91189
+ createdAt: now,
91190
+ updatedAt: now
91191
+ }, this.eventFactory("message.replied"));
91192
+ await this.pruneEvents(db);
91193
+ return message;
91194
+ }
91195
+ async get(id) {
91196
+ return new InboxDao(await this.deps.getDb()).get(id);
91197
+ }
91198
+ async claim(member, limit) {
91199
+ const db = await this.deps.getDb();
91200
+ const claimed = await new InboxDao(db).claim(member, limit, this.clock(), this.eventFactory("message.claimed"));
91201
+ await this.pruneEvents(db);
91202
+ return claimed;
91203
+ }
91204
+ async settle(ids, outcome, error) {
91205
+ if (ids.length === 0)
91206
+ return [];
91207
+ const db = await this.deps.getDb();
91208
+ const settled = await new InboxDao(db).settle(ids, outcome === "accepted" ? "delivered" : "failed", this.clock(), this.eventFactory(outcome === "accepted" ? "message.delivered" : "message.failed"), outcome === "accepted" ? null : error ?? "delivery did not start");
91209
+ await this.pruneEvents(db);
91210
+ return settled;
91211
+ }
91212
+ async release(ids) {
91213
+ if (ids.length === 0)
91214
+ return [];
91215
+ const db = await this.deps.getDb();
91216
+ const released = await new InboxDao(db).release(ids, this.clock(), this.eventFactory("message.requeued"));
91217
+ await this.pruneEvents(db);
91218
+ return released;
91219
+ }
91220
+ async sweep(now = this.clock()) {
91221
+ const db = await this.deps.getDb();
91222
+ const dao = new InboxDao(db);
91223
+ const runs = new CoordinationRunDao(db);
91224
+ const cutoff = now - this.leaseMs;
91225
+ const result = { requeued: [], failed: [], settled: [] };
91226
+ for (const message of await dao.listExpiredClaims(cutoff)) {
91227
+ const receipt = (await runs.listByMessageId(message.id))[0];
91228
+ if (receipt !== undefined) {
91229
+ if (receipt.status === "running")
91230
+ continue;
91231
+ const delivered = receipt.outcome === "verified" || receipt.outcome === "run-exit-only";
91232
+ const settled = await dao.settle([message.id], delivered ? "delivered" : "failed", now, this.eventFactory(delivered ? "message.delivered" : "message.failed"), delivered ? null : `run ${receipt.run_id} ${receipt.status}`);
91233
+ for (const row of settled)
91234
+ result.settled.push(row.id);
91235
+ continue;
91236
+ }
91237
+ if (message.attempts >= MAX_DELIVERY_ATTEMPTS) {
91238
+ const failed = await dao.settle([message.id], "failed", now, this.eventFactory("message.failed"), `attempts exhausted after ${MAX_DELIVERY_ATTEMPTS} deliveries`);
91239
+ for (const row of failed)
91240
+ result.failed.push(row.id);
91241
+ continue;
91242
+ }
91243
+ const requeued = await dao.release([message.id], now, this.eventFactory("message.requeued"));
91244
+ for (const row of requeued)
91245
+ result.requeued.push(row.id);
91246
+ }
91247
+ await this.pruneEvents(db);
91248
+ return result;
91249
+ }
91250
+ async markRead(id, reader) {
91251
+ const db = await this.deps.getDb();
91252
+ const receipt = await new InboxDao(db).markRead(id, reader, this.clock(), this.eventFactory("message.read"));
91253
+ if (receipt === null)
91254
+ throw new MessageNotFoundError(`message ${id} not found for ${reader}`);
91255
+ await this.pruneEvents(db);
91256
+ return { readAt: receipt.message.readAt ?? this.clock(), transitioned: receipt.transitioned };
91257
+ }
91258
+ async list(member, direction = "in", page) {
91259
+ const dao = new InboxDao(await this.deps.getDb());
91260
+ const limit = page?.limit ?? 100;
91261
+ const before = page?.cursor === undefined ? undefined : decodeCursor(page.cursor);
91262
+ const rows = await dao.list(member, direction, limit, before);
91263
+ const messages = rows.slice(0, limit);
91264
+ const last = messages.at(-1);
91265
+ return {
91266
+ messages,
91267
+ nextCursor: rows.length > limit && last !== undefined ? encodeCursor(last) : null
91268
+ };
91269
+ }
91270
+ async thread(id) {
91271
+ const dao = new InboxDao(await this.deps.getDb());
91272
+ const root = await dao.get(id);
91273
+ return dao.thread(root?.threadId ?? id);
91274
+ }
91275
+ async requests(member) {
91276
+ return new InboxDao(await this.deps.getDb()).requests(member === undefined ? undefined : [member]);
91277
+ }
91278
+ async countQueued(member) {
91279
+ return new InboxDao(await this.deps.getDb()).countQueued(member);
91280
+ }
91281
+ async members() {
91282
+ return new InboxDao(await this.deps.getDb()).distinctRecipients();
91283
+ }
91284
+ async attempts(taskId) {
91285
+ return new InboxDao(await this.deps.getDb()).attemptsForTask(taskId);
91286
+ }
91287
+ async taskAttempts(taskId) {
91288
+ return new InboxDao(await this.deps.getDb()).listByTask(taskId);
91289
+ }
91290
+ eventFactory(name) {
91291
+ return (row) => ({
91292
+ id: `sev_${crypto.randomUUID()}`,
91293
+ event_name: name,
91294
+ occurred_at: new Date(row.updatedAt).toISOString(),
91295
+ actor: name === "message.read" || name === "message.claimed" || name === "message.delivered" || name === "message.failed" || name === "message.requeued" ? row.toId : row.fromId,
91296
+ payload_json: JSON.stringify({
91297
+ msgId: row.id,
91298
+ fromId: row.fromId,
91299
+ toId: row.toId,
91300
+ threadId: row.threadId,
91301
+ status: row.status,
91302
+ createdAt: new Date(row.createdAt).toISOString()
91303
+ })
91304
+ });
91305
+ }
91306
+ async pruneEvents(db) {
91307
+ try {
91308
+ const quotas = ensureRetentionQuotaForPrefix(resolveRetentionQuotas(), "message", DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA);
91309
+ await new SystemEventDao(db).pruneQuotas(quotas, "message");
91310
+ } catch {}
91311
+ }
91312
+ }
91313
+ var OPERATOR_INBOX_ID = "operator", SPUR_INBOX_ID = "spur", RESERVED_INBOX_IDS, MAX_DELIVERY_ATTEMPTS = 3, RequestKeyConflictError2, CLAIM_LEASE_MS = 600000;
91314
+ var init_inbox_service = __esm(() => {
91315
+ init_src2();
91316
+ init_agent_coordination_service();
91317
+ init_system_event_catch_all();
91318
+ init_system_event_retention();
91319
+ RESERVED_INBOX_IDS = [OPERATOR_INBOX_ID, SPUR_INBOX_ID];
91320
+ RequestKeyConflictError2 = class RequestKeyConflictError2 extends Error {
91321
+ requestKey;
91322
+ constructor(requestKey) {
91323
+ super(`request key "${requestKey}" already names a different message`);
91324
+ this.requestKey = requestKey;
91325
+ this.name = "RequestKeyConflictError";
91326
+ }
91327
+ };
91328
+ });
91329
+
90823
91330
  // ../../packages/app/src/services/agent-coordination-service.ts
90824
91331
  import { join as join22 } from "path";
91332
+ function toInboxEntry(row) {
91333
+ return {
91334
+ id: row.id,
91335
+ fromId: row.fromId,
91336
+ body: row.body,
91337
+ status: row.status,
91338
+ createdAt: new Date(row.createdAt).toISOString(),
91339
+ inReplyTo: row.inReplyTo,
91340
+ injectAttempts: row.attempts,
91341
+ injectError: row.error,
91342
+ requestKey: row.requestKey
91343
+ };
91344
+ }
90825
91345
 
90826
91346
  class AgentCoordinationService {
90827
91347
  ctx;
@@ -90830,157 +91350,88 @@ class AgentCoordinationService {
90830
91350
  this.ctx = ctx;
90831
91351
  this.configDir = join22(ctx.cwd, ".spur", "agents");
90832
91352
  }
90833
- async sendMessage(fromId, toId, body, replyTo, requestKey) {
91353
+ async sendMessage(fromId, toId, body, replyTo, requestKey, extra) {
90834
91354
  validateAgentId(toId);
90835
91355
  if (fromId !== null)
90836
91356
  validateAgentId(fromId);
90837
91357
  if (requestKey !== undefined && requestKey.trim() === "") {
90838
91358
  throw new Error("requestKey must be a non-empty string");
90839
91359
  }
90840
- const dao = await this.inboxDao();
90841
- const keyed = requestKey !== undefined ? await dao.enqueueIdempotent(fromId, toId, body, requestKey, replyTo) : null;
90842
- const msgId = keyed?.id ?? await dao.enqueue(fromId, toId, body, replyTo);
90843
- await this.emitMessageEvent(replyTo !== undefined ? "message.replied" : "message.sent", {
90844
- msgId,
90845
- fromId,
90846
- toId,
90847
- threadId: replyTo ?? null,
90848
- createdAt: new Date().toISOString()
91360
+ const sender = fromId ?? OPERATOR_INBOX_ID;
91361
+ const service = await this.inboxService();
91362
+ if (replyTo !== undefined) {
91363
+ const reply = await service.reply(replyTo, sender, body);
91364
+ return { msgId: reply.id, toId: reply.toId, status: reply.status, injected: false };
91365
+ }
91366
+ const { message, replayed } = await service.send({
91367
+ from: sender,
91368
+ to: toId,
91369
+ body,
91370
+ kind: extra?.kind ?? "chat",
91371
+ ...requestKey !== undefined ? { requestKey } : {},
91372
+ ...extra?.taskId !== undefined ? { taskId: extra.taskId } : {},
91373
+ ...extra?.parentRunId !== undefined ? { parentRunId: extra.parentRunId } : {}
90849
91374
  });
90850
- return keyed === null ? { msgId, toId, status: "queued", injected: false } : { msgId, toId, status: "queued", injected: false, replayed: keyed.replayed, requestKey };
91375
+ return requestKey === undefined ? { msgId: message.id, toId: message.toId, status: message.status, injected: false } : {
91376
+ msgId: message.id,
91377
+ toId: message.toId,
91378
+ status: message.status,
91379
+ injected: false,
91380
+ replayed,
91381
+ requestKey
91382
+ };
90851
91383
  }
90852
91384
  async markMessageRead(msgId, readerId) {
90853
91385
  validateAgentId(readerId);
90854
- const receipt = await (await this.inboxRecentDao()).markRead(msgId, readerId);
91386
+ const receipt = await (await this.inboxService()).markRead(msgId, readerId).catch(() => null);
90855
91387
  if (receipt === null)
90856
91388
  throw new MessageNotFoundError(`message ${msgId} not found for ${readerId}`);
90857
- const readAt = new Date(receipt.readAt).toISOString();
90858
- if (receipt.transitioned) {
90859
- await this.emitMessageEvent("message.read", {
90860
- msgId,
90861
- fromId: receipt.fromId,
90862
- toId: readerId,
90863
- threadId: receipt.inReplyTo,
90864
- createdAt: readAt
90865
- });
90866
- }
90867
- return { readAt, transitioned: receipt.transitioned };
91389
+ return { readAt: new Date(receipt.readAt).toISOString(), transitioned: receipt.transitioned };
90868
91390
  }
90869
91391
  async getInbox(agentId, limit, offset) {
90870
91392
  validateAgentId(agentId);
90871
- const dao = await this.inboxDao();
90872
- const rows = await dao.inbox(agentId, limit, offset);
90873
- return {
90874
- messages: rows.map((row) => ({
90875
- id: row.id,
90876
- fromId: row.fromId,
90877
- body: row.body,
90878
- status: row.status,
90879
- createdAt: new Date(row.createdAt).toISOString(),
90880
- inReplyTo: row.inReplyTo,
90881
- injectAttempts: row.injectAttempts,
90882
- injectError: row.injectError,
90883
- requestKey: row.requestKey
90884
- })),
90885
- count: rows.length
90886
- };
91393
+ const { messages: rows } = await (await this.inboxService()).list(agentId, "in", {
91394
+ ...limit !== undefined ? { limit } : {}
91395
+ });
91396
+ return { messages: rows.map(toInboxEntry), count: rows.length };
90887
91397
  }
90888
91398
  async getMessage(msgId) {
90889
- const dao = await this.inboxDao();
90890
- const row = await dao.getById(msgId);
90891
- if (row === undefined)
91399
+ const row = await (await this.inboxService()).get(msgId);
91400
+ if (row === null)
90892
91401
  return null;
90893
- return { id: row.id, toId: row.toId, status: row.status, requestKey: row.requestKey ?? null };
91402
+ return { id: row.id, toId: row.toId, status: row.status, requestKey: row.requestKey };
90894
91403
  }
90895
91404
  async drainPending(agentId, options) {
90896
91405
  validateAgentId(agentId);
90897
- const dao = await this.inboxDao();
90898
- const rows = await dao.drainPending(agentId, options);
90899
- return {
90900
- messages: rows.map((row) => ({
90901
- id: row.id,
90902
- fromId: row.fromId,
90903
- body: row.body,
90904
- status: row.status,
90905
- createdAt: new Date(row.createdAt).toISOString(),
90906
- inReplyTo: row.inReplyTo,
90907
- requestKey: row.requestKey
90908
- })),
90909
- count: rows.length
90910
- };
91406
+ const rows = await (await this.inboxService()).claim(agentId, options?.limit ?? 100);
91407
+ return { messages: rows.map(toInboxEntry), count: rows.length };
90911
91408
  }
90912
91409
  async releasePending(msgIds) {
90913
91410
  if (msgIds.length === 0)
90914
91411
  return 0;
90915
- const dao = await this.inboxDao();
90916
- return dao.release(msgIds);
91412
+ return (await (await this.inboxService()).release(msgIds)).length;
90917
91413
  }
90918
91414
  async settleDelivered(msgIds) {
90919
91415
  if (msgIds.length === 0)
90920
91416
  return;
90921
- const dao = await this.inboxDao();
90922
- for (const msgId of msgIds)
90923
- await dao.markDelivered(msgId);
91417
+ await (await this.inboxService()).settle(msgIds, "accepted");
90924
91418
  }
90925
91419
  async settleFailed(msgIds, error) {
90926
91420
  if (msgIds.length === 0)
90927
91421
  return;
90928
- const dao = await this.inboxDao();
90929
- for (const msgId of msgIds)
90930
- await dao.markFailed(msgId, error);
91422
+ await (await this.inboxService()).settle(msgIds, "not-started", error);
90931
91423
  }
90932
91424
  async countPending(agentId) {
90933
91425
  validateAgentId(agentId);
90934
- const dao = await this.inboxDao();
90935
- return dao.countPending(agentId);
90936
- }
90937
- async listRecent(limit = 50) {
90938
- const dao = await this.inboxRecentDao();
90939
- const rows = await dao.listRecent(limit);
90940
- if (rows.length === 0) {
90941
- return { messages: [], count: 0 };
90942
- }
90943
- const specs = await this.listAgentSpecs();
90944
- const identityById = new Map;
90945
- for (const spec of specs) {
90946
- identityById.set(spec.id, {
90947
- agentId: spec.id,
90948
- memberLabel: spec.name,
90949
- agentType: spec.type
90950
- });
90951
- }
90952
- const replyCounts = await dao.countReplies(rows.map((r) => r.id));
90953
- const resolveEndpoint = (agentId) => identityById.get(agentId) ?? { agentId };
90954
- return {
90955
- messages: rows.map((row) => {
90956
- const replyCount = replyCounts.get(row.id) ?? 0;
90957
- return {
90958
- id: row.id,
90959
- fromId: row.from_id,
90960
- toId: row.to_id,
90961
- body: row.body,
90962
- status: row.status,
90963
- createdAt: new Date(row.created_at).toISOString(),
90964
- inReplyTo: row.in_reply_to,
90965
- ...row.from_id !== null ? { from: resolveEndpoint(row.from_id) } : {},
90966
- to: resolveEndpoint(row.to_id),
90967
- hasReply: replyCount > 0,
90968
- replyCount
90969
- };
90970
- }),
90971
- count: rows.length
90972
- };
91426
+ return (await this.inboxService()).countQueued(agentId);
90973
91427
  }
90974
91428
  async replyToMessage(msgId, body) {
90975
- const dao = await this.inboxDao();
90976
- const original = await dao.getById(msgId);
90977
- if (original === undefined) {
91429
+ const service = await this.inboxService();
91430
+ const original = await service.get(msgId);
91431
+ if (original === null)
90978
91432
  throw new Error(`No message found with id "${msgId}"`);
90979
- }
90980
- if (original.fromId === null) {
90981
- throw new Error(`Message "${msgId}" has no sender to reply to (operator-originated)`);
90982
- }
90983
- return this.sendMessage(original.toId, original.fromId, body, msgId);
91433
+ const reply = await service.reply(msgId, original.toId, body);
91434
+ return { msgId: reply.id, toId: reply.toId, status: reply.status, injected: false };
90984
91435
  }
90985
91436
  async assignTask(taskId, agentId) {
90986
91437
  validateAgentId(agentId);
@@ -91032,7 +91483,13 @@ class AgentCoordinationService {
91032
91483
  for (const id of shadowed) {
91033
91484
  this.ctx.output?.error(`agent spec "${id}" under .spur/agents/ is shadowed by the agent.fleet declaration`);
91034
91485
  }
91035
- return specs;
91486
+ const addressable = specs.filter((spec) => {
91487
+ if (!isReservedInboxId(spec.id))
91488
+ return true;
91489
+ this.ctx.output?.error(`agent spec "${spec.id}" uses a reserved inbox identity and is ignored`);
91490
+ return false;
91491
+ });
91492
+ return addressable;
91036
91493
  }
91037
91494
  async listFleetMembers() {
91038
91495
  return (await this.fleetService().resolve(this.ctx.cwd)).members;
@@ -91061,27 +91518,8 @@ class AgentCoordinationService {
91061
91518
  openDb: () => this.ctx.getDb()
91062
91519
  });
91063
91520
  }
91064
- async inboxDao() {
91065
- const db = await this.ctx.getDb();
91066
- return new InboxMessageDao(db);
91067
- }
91068
- async emitMessageEvent(name, payload) {
91069
- const bus = this.ctx.eventBus;
91070
- try {
91071
- if (bus) {
91072
- bus.emit(name, { ...payload, severity: "info" });
91073
- } else {
91074
- await new SystemEventDao(await this.ctx.getDb()).insert({
91075
- id: crypto.randomUUID(),
91076
- event_name: name,
91077
- occurred_at: payload.createdAt,
91078
- actor: (name === "message.read" ? payload.toId : payload.fromId) ?? "operator",
91079
- payload_json: JSON.stringify(payload)
91080
- });
91081
- }
91082
- } catch (error) {
91083
- this.ctx.output?.error(`Message stored, but wake event failed: ${error instanceof Error ? error.message : String(error)}`);
91084
- }
91521
+ async inboxService() {
91522
+ return new InboxService({ getDb: () => this.ctx.getDb() });
91085
91523
  }
91086
91524
  emitTaskAssignedEvent(payload) {
91087
91525
  const bus = this.ctx.eventBus;
@@ -91102,16 +91540,13 @@ class AgentCoordinationService {
91102
91540
  return null;
91103
91541
  }
91104
91542
  }
91105
- async inboxRecentDao() {
91106
- const db = await this.ctx.getDb();
91107
- return new InboxRecentDao(db);
91108
- }
91109
91543
  }
91110
91544
  var MessageNotFoundError;
91111
91545
  var init_agent_coordination_service = __esm(() => {
91112
91546
  init_src2();
91113
91547
  init_dist9();
91114
91548
  init_fleet_service();
91549
+ init_inbox_service();
91115
91550
  MessageNotFoundError = class MessageNotFoundError extends Error {
91116
91551
  };
91117
91552
  });
@@ -93106,9 +93541,10 @@ async function dispatchToFleet(input, deps) {
93106
93541
  try {
93107
93542
  ({ messageId } = await deps.dispatcher.enqueue({
93108
93543
  member: member.instanceId,
93109
- fromId: `workflow-${input.runId}`,
93544
+ fromId: "spur",
93110
93545
  body,
93111
- requestKey: `${input.runId}/${input.state}`
93546
+ requestKey: `${input.runId}/${input.state}`,
93547
+ parentRunId: input.runId
93112
93548
  }));
93113
93549
  } catch (error) {
93114
93550
  return {
@@ -103549,64 +103985,58 @@ class DeliveryReconciler {
103549
103985
  constructor(ctx) {
103550
103986
  this.ctx = ctx;
103551
103987
  }
103552
- async classify(agentId) {
103553
- const db = await this.ctx.getDb();
103554
- const rows = await new InboxUnfinishedDao(db).listUnfinished(agentId);
103555
- const runs = new CoordinationRunDao(db);
103988
+ async inbox() {
103989
+ return new InboxService({ getDb: () => this.ctx.getDb() });
103990
+ }
103991
+ async classify(agentId, rows) {
103992
+ const requests = rows ?? await (await this.inbox()).requests(agentId);
103556
103993
  const unresolved = [];
103557
- for (const row of rows) {
103994
+ for (const row of requests) {
103558
103995
  const base = {
103559
103996
  messageId: row.id,
103560
- toId: row.to_id,
103561
- injectAttempts: row.inject_attempts,
103562
- requestKey: row.request_key,
103563
- ...row.inject_error !== null && row.inject_error !== "" ? { injectError: row.inject_error } : {}
103997
+ toId: row.toId,
103998
+ injectAttempts: row.attempts,
103999
+ requestKey: row.requestKey,
104000
+ ...row.error !== null && row.error !== "" ? { injectError: row.error } : {}
103564
104001
  };
103565
104002
  if (row.status === "failed") {
103566
104003
  unresolved.push({
103567
104004
  ...base,
103568
- reason: row.inject_attempts >= MAX_INJECT_ATTEMPTS ? "attempts-exhausted" : "delivery-failed",
104005
+ reason: row.attempts >= MAX_DELIVERY_ATTEMPTS ? "attempts-exhausted" : "delivery-failed",
103569
104006
  artifacts: []
103570
104007
  });
103571
104008
  continue;
103572
104009
  }
103573
- if (row.status === "injected" || row.status === "delivered") {
103574
- const runRow = (await runs.listByMessageId(row.id))[0];
103575
- if (runRow === undefined) {
104010
+ if (row.status === "claimed" || row.status === "delivered") {
104011
+ if (row.runId === null) {
103576
104012
  unresolved.push({ ...base, reason: "outcome-unknown", artifacts: [] });
103577
104013
  continue;
103578
104014
  }
103579
- const completed = runRow.completed_at !== null && runRow.status !== "running";
103580
- if (completed && runRow.outcome === "verified")
104015
+ const completed = row.runStatus !== "running";
104016
+ if (completed && row.runOutcome === "verified")
103581
104017
  continue;
103582
104018
  unresolved.push({
103583
104019
  ...base,
103584
- reason: !completed ? "outcome-unknown" : runRow.outcome === "errored" ? "delivery-failed" : runRow.outcome === "run-exit-only" ? "run-exit-only" : "outcome-unknown",
103585
- runId: runRow.run_id,
103586
- runStatus: runRow.status,
103587
- ...runRow.task_id !== null ? { taskId: runRow.task_id } : {},
103588
- artifacts: parseArtifactRefs2(runRow.artifact_refs_json)
104020
+ reason: !completed ? "outcome-unknown" : row.runOutcome === "errored" ? "delivery-failed" : row.runOutcome === "run-exit-only" ? "run-exit-only" : "outcome-unknown",
104021
+ runId: row.runId,
104022
+ ...row.runStatus !== null ? { runStatus: row.runStatus } : {},
104023
+ ...row.runTaskId !== null ? { taskId: row.runTaskId } : {},
104024
+ artifacts: parseArtifactRefs2(row.runArtifactRefsJson)
103589
104025
  });
103590
104026
  }
103591
104027
  }
103592
104028
  return unresolved;
103593
104029
  }
103594
104030
  async reconcile(agentId) {
103595
- const db = await this.ctx.getDb();
103596
- const rows = await new InboxUnfinishedDao(db).listUnfinished(agentId);
103597
- const inbox = new InboxMessageDao(db);
103598
- const report = { unresolved: [], exhausted: [], scanned: rows.length };
103599
- for (const row of rows) {
103600
- if (row.status === "queued" && row.inject_attempts >= MAX_INJECT_ATTEMPTS) {
103601
- await inbox.markFailed(row.id, `attempts exhausted after ${MAX_INJECT_ATTEMPTS} deliveries`);
103602
- report.exhausted.push(row.id);
103603
- }
103604
- }
103605
- report.unresolved = await this.classify(agentId);
103606
- return report;
104031
+ const service = await this.inbox();
104032
+ const swept = await service.sweep();
104033
+ const rows = await service.requests(agentId);
104034
+ return { unresolved: await this.classify(agentId, rows), exhausted: swept.failed, scanned: rows.length };
103607
104035
  }
103608
104036
  }
103609
104037
  function parseArtifactRefs2(artifactRefsJson) {
104038
+ if (artifactRefsJson === null)
104039
+ return [];
103610
104040
  try {
103611
104041
  const parsed = JSON.parse(artifactRefsJson);
103612
104042
  return Array.isArray(parsed) ? parsed : [];
@@ -103614,9 +104044,8 @@ function parseArtifactRefs2(artifactRefsJson) {
103614
104044
  return [];
103615
104045
  }
103616
104046
  }
103617
- var MAX_INJECT_ATTEMPTS = 3;
103618
104047
  var init_delivery_reconciler = __esm(() => {
103619
- init_src2();
104048
+ init_inbox_service();
103620
104049
  });
103621
104050
 
103622
104051
  // ../../packages/app/src/services/execution-policy.ts
@@ -104683,7 +105112,11 @@ class FleetDispatcher {
104683
105112
  this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
104684
105113
  }
104685
105114
  async enqueue(req) {
104686
- const sent = await this.deps.coordination.sendMessage(req.fromId, req.member, req.body, undefined, req.requestKey);
105115
+ const sent = await this.deps.coordination.sendMessage(req.fromId, req.member, req.body, undefined, req.requestKey, {
105116
+ kind: "request",
105117
+ ...req.taskId !== undefined ? { taskId: req.taskId } : {},
105118
+ ...req.parentRunId !== undefined ? { parentRunId: req.parentRunId } : {}
105119
+ });
104687
105120
  return { messageId: sent.msgId, replayed: sent.replayed === true };
104688
105121
  }
104689
105122
  async receipt(messageId, member) {
@@ -104781,6 +105214,13 @@ class FleetGuestService {
104781
105214
  const joined = new Set(guests.map((g) => g.id));
104782
105215
  const requested = input.id?.trim();
104783
105216
  const id = requested !== undefined && requested !== "" ? requested : this.nextGuestId(role, declared, joined);
105217
+ if (isReservedInboxId(id)) {
105218
+ return {
105219
+ ok: false,
105220
+ code: "collision",
105221
+ message: `guest id "${id}" is a reserved inbox identity \u2014 pass a free --id`
105222
+ };
105223
+ }
104784
105224
  const own = guests.find((g) => g.id === id);
104785
105225
  if (own !== undefined && own.sessionId !== null && own.sessionId === (input.sessionId ?? null)) {
104786
105226
  if (await this.heartbeat(id))
@@ -104854,7 +105294,10 @@ class FleetGuestService {
104854
105294
  return expired;
104855
105295
  }
104856
105296
  async pendingCount(id) {
104857
- return await new InboxMessageDao(await this.ctx.getDb()).countPending(id);
105297
+ return await this.inbox().countQueued(id);
105298
+ }
105299
+ inbox() {
105300
+ return new InboxService({ getDb: () => this.ctx.getDb() });
104858
105301
  }
104859
105302
  async retire(record) {
104860
105303
  const db = await this.ctx.getDb();
@@ -104862,10 +105305,9 @@ class FleetGuestService {
104862
105305
  if (claim !== null) {
104863
105306
  await new ProjectClaimDao(db).release(this.ctx.cwd, this.slot(record.id), record.id, claim.ownerEpoch);
104864
105307
  }
104865
- const unfinished = await new InboxUnfinishedDao(db).listUnfinished(record.id);
104866
- const claimed = unfinished.filter((row) => row.status === "injected").map((row) => row.id);
105308
+ const claimed = (await this.inbox().list(record.id, "in", { limit: 500 })).messages.filter((row) => row.status === "claimed").map((row) => row.id);
104867
105309
  if (claimed.length > 0)
104868
- await new InboxMessageDao(db).release(claimed);
105310
+ await this.inbox().release(claimed);
104869
105311
  if (record.runId !== "") {
104870
105312
  await new CoordinationRunDao(db).updateExit(record.runId, "exited", new Date().toISOString(), "[]", {
104871
105313
  messageIds: [],
@@ -104894,6 +105336,7 @@ class FleetGuestService {
104894
105336
  var GUEST_LEASE_TTL_MS, GUEST_ROLES, GUEST_RECORD_DIR, GUEST_CLAIM_PREFIX = "guest:";
104895
105337
  var init_fleet_guest_service = __esm(() => {
104896
105338
  init_src2();
105339
+ init_inbox_service();
104897
105340
  GUEST_LEASE_TTL_MS = CLAIM_TTL_MS;
104898
105341
  GUEST_ROLES = ["scribe", "coder", "reviewer", "planner"];
104899
105342
  GUEST_RECORD_DIR = join44(".spur", "run", "guests");
@@ -108041,13 +108484,13 @@ class JobWorkerService {
108041
108484
 
108042
108485
  // ../../packages/app/src/services/occupant-wait.ts
108043
108486
  function projectLifecycle(input) {
108044
- const { latestInvokeEvent, pendingCount } = input;
108487
+ const { latestInvokeEvent, queuedCount } = input;
108045
108488
  if (latestInvokeEvent === null)
108046
108489
  return "unknown";
108047
108490
  if (latestInvokeEvent.eventName === "agent.invoke.start")
108048
108491
  return "working";
108049
108492
  if (latestInvokeEvent.eventName === "agent.invoke.exit") {
108050
- return pendingCount === 0 ? "idle" : "unknown";
108493
+ return queuedCount === 0 ? "idle" : "unknown";
108051
108494
  }
108052
108495
  return "unknown";
108053
108496
  }
@@ -108072,10 +108515,10 @@ async function snapshotOccupant(deps, specId, opts) {
108072
108515
  if (opts.runId !== undefined && opts.runId !== occupant.runId) {}
108073
108516
  const pin = { specId, runId, generation: occupant.generation };
108074
108517
  const latest = await deps.latestInvokeEvent(runId);
108075
- const pendingCount = await deps.countPending(specId);
108518
+ const queuedCount = await deps.countQueued(specId);
108076
108519
  return {
108077
108520
  pin,
108078
- lifecycle: projectLifecycle({ latestInvokeEvent: latest, pendingCount }),
108521
+ lifecycle: projectLifecycle({ latestInvokeEvent: latest, queuedCount }),
108079
108522
  startClock: deps.now(),
108080
108523
  latestInvoke: latest
108081
108524
  };
@@ -108126,8 +108569,8 @@ async function waitForOccupant(deps, opts) {
108126
108569
  if (occupant.generation > pin.generation) {
108127
108570
  throw new WaitError("run_replaced", `generation ${pin.generation} \u2192 ${occupant.generation}`);
108128
108571
  }
108129
- const pendingCount = await deps.countPending(pin.specId);
108130
- const lifecycle = projectLifecycle({ latestInvokeEvent: latestInvoke, pendingCount });
108572
+ const queuedCount = await deps.countQueued(pin.specId);
108573
+ const lifecycle = projectLifecycle({ latestInvokeEvent: latestInvoke, queuedCount });
108131
108574
  if (until === "invoke-exit") {
108132
108575
  if (latestInvoke !== null && latestInvoke.eventName === "agent.invoke.exit") {
108133
108576
  return { pin, satisfied: until };
@@ -110581,18 +111024,6 @@ import { randomUUID as randomUUID7 } from "crypto";
110581
111024
  function fleetTaskKeyPrefix(wbs) {
110582
111025
  return `${FLEET_TASK_KEY_PREFIX}${wbs}:`;
110583
111026
  }
110584
- function parseFleetTaskAttempt(requestKey, wbs) {
110585
- const prefix = fleetTaskKeyPrefix(wbs);
110586
- if (!requestKey.startsWith(prefix))
110587
- return null;
110588
- const suffix = requestKey.slice(prefix.length);
110589
- if (!/^[1-9][0-9]*$/.test(suffix))
110590
- return null;
110591
- return Number.parseInt(suffix, 10);
110592
- }
110593
- function fleetTaskWbs(requestKey) {
110594
- return /^fleet:task:([^:]+):[1-9][0-9]*$/.exec(requestKey ?? "")?.[1];
110595
- }
110596
111027
 
110597
111028
  class StrategyRuntime {
110598
111029
  ctx;
@@ -110640,6 +111071,15 @@ class StrategyRuntime {
110640
111071
  for (const member of (await this.ctx.fleet.resolve(normalized)).members) {
110641
111072
  unresolved.push(...options.readOnly ? await reconciler.classify(member.instanceId) : (await reconciler.reconcile(member.instanceId)).unresolved);
110642
111073
  }
111074
+ if (!options.readOnly && this.ctx.guestExpiry !== undefined) {
111075
+ try {
111076
+ await this.ctx.guestExpiry();
111077
+ } catch (error) {
111078
+ this.ctx.logger?.warn("strategy resume: guest expiry failed", {
111079
+ error: error instanceof Error ? error.message : String(error)
111080
+ });
111081
+ }
111082
+ }
110643
111083
  return { strategy, version, orchestrator, unresolved, reconciled: true };
110644
111084
  }
110645
111085
  async tick(projectPath, opts) {
@@ -110662,7 +111102,7 @@ class StrategyRuntime {
110662
111102
  };
110663
111103
  }
110664
111104
  const db = await this.ctx.openDb(normalized);
110665
- const inbox = new InboxUnfinishedDao(db);
111105
+ const inbox = new InboxService({ getDb: async () => db });
110666
111106
  const slots = new WriteSlotService(this.ctx);
110667
111107
  const dispatched = [];
110668
111108
  for (const decision of result.decisions) {
@@ -110685,7 +111125,8 @@ class StrategyRuntime {
110685
111125
  member: decision.instanceId,
110686
111126
  fromId: opts.orchestratorId,
110687
111127
  body: `/sp:dev-run ${wbs} --auto${resumeHint ? " --continue" : ""}`,
110688
- requestKey: `${fleetTaskKeyPrefix(wbs)}${attempt}`
111128
+ requestKey: `${fleetTaskKeyPrefix(wbs)}${attempt}`,
111129
+ taskId: wbs
110689
111130
  });
110690
111131
  dispatched.push(decision);
110691
111132
  this.startHeartbeat(normalized, decision.instanceId, opts.ownerEpoch);
@@ -110722,8 +111163,8 @@ class StrategyRuntime {
110722
111163
  const dispatcher = this.ctx.dispatcher;
110723
111164
  if (dispatcher === undefined)
110724
111165
  return { released: [] };
110725
- const rows = await new InboxUnfinishedDao(db).listUnfinished(holder.holderId);
110726
- const pending = rows.find((row) => (row.request_key ?? "").startsWith("fleet:task:"));
111166
+ const rows = await new InboxService({ getDb: async () => db }).requests(holder.holderId);
111167
+ const pending = rows.find((row) => row.taskId !== null);
110727
111168
  if (pending === undefined)
110728
111169
  return { released: [] };
110729
111170
  const receipt = await dispatcher.receipt(pending.id, holder.holderId);
@@ -110732,7 +111173,7 @@ class StrategyRuntime {
110732
111173
  this.stopHeartbeat(normalized);
110733
111174
  const slots = new WriteSlotService(this.ctx);
110734
111175
  await slots.validateResult(normalized, holder.holderId, holder.ownerEpoch, {
110735
- taskId: fleetTaskWbs(pending.request_key)
111176
+ taskId: pending.taskId ?? undefined
110736
111177
  });
110737
111178
  const released = await slots.release(normalized, holder.holderId, holder.ownerEpoch);
110738
111179
  return { released: released ? [holder.holderId] : [] };
@@ -110806,7 +111247,7 @@ class StrategyRuntime {
110806
111247
  idleInstances.push(member);
110807
111248
  }
110808
111249
  }
110809
- const inbox = new InboxUnfinishedDao(db);
111250
+ const inbox = new InboxService({ getDb: async () => db });
110810
111251
  const memberLifecycle = new Map;
110811
111252
  if (idleInstances.length > 0) {
110812
111253
  try {
@@ -110823,7 +111264,7 @@ class StrategyRuntime {
110823
111264
  const latest = attempts.latest;
110824
111265
  let state;
110825
111266
  if (latest !== null) {
110826
- const receipt = await this.ctx.dispatcher?.receipt(latest.row.id, latest.row.to_id);
111267
+ const receipt = await this.ctx.dispatcher?.receipt(latest.row.id, latest.row.toId);
110827
111268
  state = {
110828
111269
  attempt: latest.attempt,
110829
111270
  ...receipt !== null && receipt !== undefined && receipt.status !== "outcome-unknown" ? { receipt: receipt.status } : {}
@@ -110854,22 +111295,18 @@ class StrategyRuntime {
110854
111295
  return { name, result, statusByWbs };
110855
111296
  }
110856
111297
  async readAttempts(inbox, wbs) {
110857
- const rows = await inbox.listByRequestKeyPrefix(fleetTaskKeyPrefix(wbs));
110858
- let latest = null;
110859
- for (const row of rows) {
110860
- const attempt = parseFleetTaskAttempt(row.request_key ?? "", wbs);
110861
- if (attempt === null)
110862
- continue;
110863
- if (latest === null || attempt > latest.attempt)
110864
- latest = { row, attempt };
110865
- }
110866
- return { count: rows.length, latest };
111298
+ const attempts = await inbox.attempts(wbs);
111299
+ return {
111300
+ count: attempts.count,
111301
+ latest: attempts.latest === null ? null : { row: attempts.latest, attempt: attempts.count }
111302
+ };
110867
111303
  }
110868
111304
  }
110869
111305
  var DEFAULT_STRATEGY = "rest", FLEET_AUTO_TAG = "fleet:auto", MAX_DISPATCH_ATTEMPTS = 3, MAX_HOLD_DETAIL_IDS = 5, FLEET_TASK_KEY_PREFIX = "fleet:task:", restStrategy, gtdStrategy, STRATEGIES;
110870
111306
  var init_strategy_runtime = __esm(() => {
110871
111307
  init_src2();
110872
111308
  init_delivery_reconciler();
111309
+ init_inbox_service();
110873
111310
  init_project_registry();
110874
111311
  init_write_slot_service();
110875
111312
  restStrategy = {
@@ -111246,220 +111683,6 @@ var init_supervisor_service = __esm(() => {
111246
111683
  RESTART_BACKOFF_SCHEDULE = [1000, 2000, 4000, 8000, 16000];
111247
111684
  });
111248
111685
 
111249
- // ../../packages/app/src/services/system-event-retention.ts
111250
- function resolveRetentionQuotas(config = {}, prefixes = SYSTEM_EVENT_PREFIXES) {
111251
- const defaultQuota = config.default ?? DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA;
111252
- const overrides = config.prefixes ?? {};
111253
- return prefixes.map((prefix) => ({
111254
- prefix,
111255
- quota: overrides[prefix] ?? defaultQuota
111256
- }));
111257
- }
111258
- var DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA = 1e4;
111259
- var init_system_event_retention = __esm(() => {
111260
- init_event_names();
111261
- });
111262
-
111263
- // ../../packages/app/src/services/system-event-tap.ts
111264
- function registerSystemEventTap(bus, dao, logger, options = {}) {
111265
- const handlers = new Map;
111266
- const inFlight = new Set;
111267
- const diagnosticEnabled = options.diagnosticEnabled === true;
111268
- const quotas = resolveRetentionQuotas(options.retention);
111269
- for (const entry of SYSTEM_EVENT_CATALOG) {
111270
- if (entry.tier === "diagnostic" && !diagnosticEnabled)
111271
- continue;
111272
- const handler = (event) => {
111273
- const actor = extractSystemEventActor(event);
111274
- const p = persist(dao, entry.prefix, {
111275
- id: createId("sev"),
111276
- event_name: entry.name,
111277
- occurred_at: new Date().toISOString(),
111278
- actor,
111279
- payload_json: safeStringify(buildSystemEventEnvelope(entry, event, options.projectContext ?? systemEventProjectContext(""), options.secretValues, actor)),
111280
- ...extractSystemEventCorrelation(event)
111281
- }, quotas, logger);
111282
- inFlight.add(p);
111283
- p.finally(() => inFlight.delete(p));
111284
- };
111285
- handlers.set(entry.name, handler);
111286
- bus.on(entry.name, handler);
111287
- }
111288
- return {
111289
- unsubscribe: () => {
111290
- for (const [name, handler] of handlers) {
111291
- bus.off(name, handler);
111292
- }
111293
- handlers.clear();
111294
- },
111295
- flush: async () => {
111296
- while (inFlight.size > 0) {
111297
- await Promise.allSettled(inFlight);
111298
- }
111299
- }
111300
- };
111301
- }
111302
- async function persist(dao, prefix, input, quotas, logger) {
111303
- try {
111304
- await dao.insert(input);
111305
- await dao.pruneQuotas(quotas, prefix);
111306
- } catch (error) {
111307
- logger.warn("system_events tap: persist failed", {
111308
- name: input.event_name,
111309
- error: stringifyError(error)
111310
- });
111311
- }
111312
- }
111313
- function safeStringify(event) {
111314
- try {
111315
- return JSON.stringify(event ?? null);
111316
- } catch {
111317
- return null;
111318
- }
111319
- }
111320
- function extractSystemEventActor(event) {
111321
- if (event && typeof event === "object") {
111322
- const obj = event;
111323
- if (typeof obj.actor === "string" && obj.actor.length > 0)
111324
- return obj.actor;
111325
- if (typeof obj.agentId === "string" && obj.agentId.length > 0)
111326
- return obj.agentId;
111327
- if (typeof obj.memberId === "string" && obj.memberId.length > 0)
111328
- return obj.memberId;
111329
- }
111330
- return null;
111331
- }
111332
- function extractSystemEventCorrelation(event) {
111333
- const correlation = { run_id: null, entity_kind: null, entity_id: null, sequence: null };
111334
- if (!event || typeof event !== "object")
111335
- return correlation;
111336
- const obj = event;
111337
- const nested = obj.correlation && typeof obj.correlation === "object" ? obj.correlation : {};
111338
- const runId = obj.runId ?? obj.run_id ?? nested.runId;
111339
- if (typeof runId === "string" && runId.length > 0)
111340
- correlation.run_id = runId;
111341
- if (typeof obj.sequence === "number" && Number.isFinite(obj.sequence))
111342
- correlation.sequence = obj.sequence;
111343
- const entity = obj.entity;
111344
- if (entity && typeof entity === "object") {
111345
- const { kind, id } = entity;
111346
- if (typeof kind === "string" && kind.length > 0)
111347
- correlation.entity_kind = kind;
111348
- if (typeof id === "string" && id.length > 0)
111349
- correlation.entity_id = id;
111350
- }
111351
- if (typeof obj.entityKind === "string" && obj.entityKind.length > 0)
111352
- correlation.entity_kind = obj.entityKind;
111353
- if (typeof obj.entityId === "string" && obj.entityId.length > 0)
111354
- correlation.entity_id = obj.entityId;
111355
- return correlation;
111356
- }
111357
- function stringifyError(error) {
111358
- if (error instanceof Error)
111359
- return `${error.name}: ${error.message}`;
111360
- return String(error);
111361
- }
111362
- var init_system_event_tap = __esm(() => {
111363
- init_src2();
111364
- init_event_names();
111365
- init_system_event_envelope();
111366
- init_system_event_retention();
111367
- });
111368
-
111369
- // ../../packages/app/src/services/system-event-catch-all.ts
111370
- function genericSystemEventCatalogEntry(name) {
111371
- return {
111372
- name,
111373
- prefix: name.replace(/\..*/, ""),
111374
- source: "bus",
111375
- tier: "default",
111376
- persisted: true,
111377
- streamed: true,
111378
- payloadPolicy: "metadata-only",
111379
- renderer: GENERIC_SYSTEM_EVENT_RENDERER,
111380
- producerPackage: "spur",
111381
- subsystem: "unknown",
111382
- severity: "info",
111383
- description: "Uncataloged system event captured by the generic fallback; pending catalog promotion.",
111384
- metadataFields: [],
111385
- remediationKind: "none"
111386
- };
111387
- }
111388
- function ensureRetentionQuotaForPrefix(quotas, prefix, fallbackQuota) {
111389
- return quotas.some((quota) => quota.prefix === prefix) ? quotas : [...quotas, { prefix, quota: fallbackQuota }];
111390
- }
111391
- function createSystemEventCatchAllSink(options) {
111392
- const { dao, logger } = options;
111393
- const quotas = resolveRetentionQuotas(options.retention);
111394
- const fallbackQuota = options.retention?.default ?? DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA;
111395
- const projectContext = options.projectContext ?? systemEventProjectContext("");
111396
- const secretValues = options.secretValues ?? [];
111397
- const driftWarned = new Set;
111398
- return async (name, payload) => {
111399
- const entry = genericSystemEventCatalogEntry(name);
111400
- const actor = extractSystemEventActor(payload);
111401
- try {
111402
- await dao.insert({
111403
- id: createId("sev"),
111404
- event_name: entry.name,
111405
- occurred_at: new Date().toISOString(),
111406
- actor,
111407
- payload_json: safeStringify(buildSystemEventEnvelope(entry, payload, projectContext, secretValues, actor)),
111408
- ...extractSystemEventCorrelation(payload)
111409
- });
111410
- await dao.pruneQuotas(ensureRetentionQuotaForPrefix(quotas, entry.prefix, fallbackQuota), entry.prefix);
111411
- } catch (error) {
111412
- logger.warn("system_events catch-all: persist failed", {
111413
- event: name,
111414
- error: error instanceof Error ? `${error.name}: ${error.message}` : String(error)
111415
- });
111416
- return;
111417
- }
111418
- if (!driftWarned.has(name)) {
111419
- driftWarned.add(name);
111420
- logger.warn(SYSTEM_EVENT_UNCATALOGED_WARN_EVENT, { event: name });
111421
- }
111422
- };
111423
- }
111424
- function installSystemEventCatchAll(bus, sink) {
111425
- const holder = bus;
111426
- const current = holder.emit;
111427
- if (current[CATCH_ALL_MARKER] === true) {
111428
- return { installed: false, flush: async () => {} };
111429
- }
111430
- const underlying = current.bind(bus);
111431
- const inFlight = new Set;
111432
- const wrapped = (event, ...args) => {
111433
- const result = underlying(event, ...args);
111434
- if (systemEventCatalogEntry(event) === undefined) {
111435
- const pending = Promise.resolve().then(() => sink(event, args[0]));
111436
- inFlight.add(pending);
111437
- pending.catch(() => {}).finally(() => {
111438
- inFlight.delete(pending);
111439
- });
111440
- }
111441
- return result;
111442
- };
111443
- Object.defineProperty(wrapped, CATCH_ALL_MARKER, { value: true });
111444
- holder.emit = wrapped;
111445
- return {
111446
- installed: true,
111447
- flush: async () => {
111448
- while (inFlight.size > 0) {
111449
- await Promise.allSettled([...inFlight]);
111450
- }
111451
- }
111452
- };
111453
- }
111454
- var GENERIC_SYSTEM_EVENT_RENDERER = "generic", SYSTEM_EVENT_UNCATALOGED_WARN_EVENT = "system_events.uncataloged", CATCH_ALL_MARKER = "spur.systemEventCatchAll";
111455
- var init_system_event_catch_all = __esm(() => {
111456
- init_src2();
111457
- init_event_names();
111458
- init_system_event_envelope();
111459
- init_system_event_retention();
111460
- init_system_event_tap();
111461
- });
111462
-
111463
111686
  // ../../packages/app/src/services/system-event-emitter.ts
111464
111687
  class SystemEventEmitter {
111465
111688
  dao;
@@ -120938,11 +121161,16 @@ async function persistWorktreeRuns(input) {
120938
121161
  if (fromStat === undefined || !fromStat.isDirectory()) {
120939
121162
  throw new Error(`persist-out: source worktree does not exist or is not a directory: ${input.fromWorkdir}`);
120940
121163
  }
121164
+ const fs = createNodeFileSystem2();
121165
+ const fromReal = fs.realPath?.(fromDir) ?? fromDir;
121166
+ const toReal = fs.realPath?.(toDir) ?? toDir;
121167
+ if (fromReal === toReal) {
121168
+ throw new Error(`persist-out: source and destination are the same tree (${fromDir}) \u2014 run it from the invoking tree, or the evidence is copied onto itself and lost with the worktree`);
121169
+ }
120941
121170
  const fromRunDir = join67(fromDir, ".spur", "run");
120942
121171
  const toRunDir = join67(toDir, ".spur", "run");
120943
121172
  const fromRecordsDir = runStoragePaths(fromDir).recordsDir;
120944
121173
  const toRecordsDir = runStoragePaths(toDir).recordsDir;
120945
- const fs = createNodeFileSystem2();
120946
121174
  const fromEvidence = runStoragePaths(fromDir).evidenceDir;
120947
121175
  const toEvidence = runStoragePaths(toDir).evidenceDir;
120948
121176
  const evidenceCopies = [];
@@ -122377,6 +122605,7 @@ var init_src3 = __esm(() => {
122377
122605
  init_history_board_service();
122378
122606
  init_history_refresh_service();
122379
122607
  init_history_service();
122608
+ init_inbox_service();
122380
122609
  init_inline_run_attribution();
122381
122610
  init_job_exclusion_guard();
122382
122611
  init_member_session2();
@@ -142511,27 +142740,26 @@ async function runAgentLifecycle(action, agentId, options, context) {
142511
142740
  async function settleClaimedMessages(context, claimed, outcome) {
142512
142741
  if (claimed.length === 0)
142513
142742
  return;
142514
- const coordination = new AgentCoordinationService(context);
142743
+ const inbox = new InboxService({ getDb: () => context.getDb() });
142515
142744
  if (outcome === "accepted") {
142516
- await coordination.settleDelivered(claimed);
142745
+ await inbox.settle(claimed, "accepted");
142517
142746
  return;
142518
142747
  }
142519
- const dao = new InboxMessageDao(await context.getDb());
142520
142748
  const releasable = [];
142521
142749
  const exhausted = [];
142522
142750
  for (const id of claimed) {
142523
- const row = await dao.getById(id);
142524
- if (row?.status !== "injected")
142751
+ const row = await inbox.get(id);
142752
+ if (row?.status !== "claimed")
142525
142753
  continue;
142526
- if (row.injectAttempts >= MAX_INJECT_ATTEMPTS)
142754
+ if (row.attempts >= MAX_DELIVERY_ATTEMPTS)
142527
142755
  exhausted.push(id);
142528
142756
  else
142529
142757
  releasable.push(id);
142530
142758
  }
142531
142759
  if (releasable.length > 0)
142532
- await coordination.releasePending(releasable);
142533
- for (const id of exhausted) {
142534
- await coordination.settleFailed([id], `delivery not accepted: invocation never started after ${MAX_INJECT_ATTEMPTS} inject attempts`);
142760
+ await inbox.release(releasable);
142761
+ if (exhausted.length > 0) {
142762
+ await inbox.settle(exhausted, "not-started", `delivery not accepted: invocation never started after ${MAX_DELIVERY_ATTEMPTS} inject attempts`);
142535
142763
  }
142536
142764
  }
142537
142765
  async function drainIntoPrompt(prompt, context, flags) {
@@ -142547,21 +142775,21 @@ async function drainIntoPrompt(prompt, context, flags) {
142547
142775
  const flagsOut = spec === undefined ? flags : { ...flags, "spec-id": spec.id, agent: drainAgentSelector(spec, context) };
142548
142776
  if (flags.drain !== true)
142549
142777
  return { prompt, flags: flagsOut, claimed: [] };
142550
- const inbox = await coordination.drainPending(recipient, flags.singleMessage === true ? { limit: 1 } : undefined);
142551
- if (inbox.count === 0)
142778
+ const claimedRows = await new InboxService({ getDb: () => context.getDb() }).claim(recipient, flags.singleMessage === true ? 1 : 100);
142779
+ if (claimedRows.length === 0)
142552
142780
  return { prompt, flags: flagsOut, claimed: [] };
142553
- const header = inbox.messages.map((m) => `- ${m.fromId ?? "operator"}: ${m.body}`).join(`
142781
+ const header = claimedRows.map((m) => `- ${m.fromId || "operator"}: ${m.body}`).join(`
142554
142782
  `);
142555
142783
  const block = `Pending messages:
142556
142784
  ${header}`;
142557
142785
  const merged = prompt === undefined ? block : `${block}
142558
142786
 
142559
142787
  ${prompt}`;
142560
- const claimed = inbox.messages.map((m) => m.id);
142561
- const fleetTask = inbox.messages.map((message) => /^fleet:task:([^:]+):[1-9][0-9]*$/.exec(message.requestKey ?? "")?.[1]).find((wbs) => wbs !== undefined);
142562
- const parentRunId = inbox.messages.map((message) => /^([^:/][^/]*)\/[^/]+$/.exec(message.requestKey ?? "")?.[1]).find((runId) => runId !== undefined);
142788
+ const claimed = claimedRows.map((m) => m.id);
142789
+ const fleetTask = claimedRows.map((message) => message.taskId ?? /^fleet:task:([^:]+):[1-9][0-9]*$/.exec(message.requestKey ?? "")?.[1]).find((wbs) => wbs !== undefined);
142790
+ const parentRunId = claimedRows.map((message) => message.parentRunId ?? /^([^:/][^/]*)\/[^/]+$/.exec(message.requestKey ?? "")?.[1]).find((runId) => runId !== undefined);
142563
142791
  const requestMessage = claimed.join(",");
142564
- const requestKeys = inbox.messages.map((message) => message.requestKey ?? null);
142792
+ const requestKeys = claimedRows.map((message) => message.requestKey);
142565
142793
  return {
142566
142794
  prompt: merged,
142567
142795
  flags: {
@@ -142630,7 +142858,8 @@ async function makeFleetRuntime(context) {
142630
142858
  const { foldersConfig } = await resolvePlanningFolders(context.fs);
142631
142859
  const check = await makeCheckService(context);
142632
142860
  return await check.firstBlockingPrerequisite(context.fs.resolve(foldersConfig.active_folder), wbs);
142633
- }
142861
+ },
142862
+ guestExpiry: async () => await guestService(context).expire()
142634
142863
  });
142635
142864
  }
142636
142865
  async function runAgentLoop(context, flags, runtime = {}, deps) {
@@ -143272,7 +143501,7 @@ async function runAgentWait(context, specId, options) {
143272
143501
  return waitUsageError(context, options, "--until blocked has no first-class signal in this wave; use idle|working|invoke-exit");
143273
143502
  }
143274
143503
  const agentService = context.agentService();
143275
- const coordination = new AgentCoordinationService(context);
143504
+ const inbox = new InboxService({ getDb: () => context.getDb() });
143276
143505
  const eventDao = new SystemEventDao(await context.getDb());
143277
143506
  const controller = new AbortController;
143278
143507
  const onSignal = () => controller.abort();
@@ -143292,7 +143521,7 @@ async function runAgentWait(context, specId, options) {
143292
143521
  try {
143293
143522
  result = await waitForOccupant({
143294
143523
  getOccupant: (id) => agentService.getOccupant({ specId: id }),
143295
- countPending: (id) => coordination.countPending(id),
143524
+ countQueued: (id) => inbox.countQueued(id),
143296
143525
  latestInvokeEvent: (r) => readLatestInvokeEvent(eventDao, r),
143297
143526
  follow: (afterSequence) => followSystemEventsAfter(context.getDb, {
143298
143527
  afterSequence,
@@ -146882,7 +147111,7 @@ import { createRequire as createRequire3 } from "module";
146882
147111
  var CLI_CONFIG = {
146883
147112
  binaryName: "spur",
146884
147113
  binaryLabel: "spur",
146885
- binaryVersion: "0.4.17",
147114
+ binaryVersion: "0.4.19",
146886
147115
  configDir: ".spur",
146887
147116
  configFile: ".spur/config.yaml",
146888
147117
  databaseFile: ".spur/spur.db"
@@ -147817,13 +148046,231 @@ function formatMaintenanceResult(r) {
147817
148046
  `);
147818
148047
  }
147819
148048
 
147820
- // src/commands/message.ts
148049
+ // src/commands/message/inbox.ts
148050
+ init_src3();
148051
+
148052
+ // src/commands/message/shared.ts
147821
148053
  init_src3();
147822
- init_src2();
147823
148054
  var DEFAULT_FROM = "operator";
147824
148055
  var DEFAULT_WATCH_INTERVAL_MS = 2000;
147825
- function registerMessageCommand(program, context) {
147826
- const noun = program.command("message").summary("send and inspect durable inter-agent messages");
148056
+ function inboxOf(context) {
148057
+ return new InboxService({ getDb: () => context.getDb() });
148058
+ }
148059
+ async function resolveOrchestratorAddress(context) {
148060
+ let fleet;
148061
+ try {
148062
+ fleet = new FleetService({
148063
+ fs: context.fs,
148064
+ spurConfig: await context.loadAgentConfig(context.cwd),
148065
+ roles: context.agentRoles,
148066
+ openDb: () => context.getDb()
148067
+ });
148068
+ } catch (error) {
148069
+ const message = error instanceof Error ? error.message : String(error);
148070
+ throw new Error(`message send could not load the project config: ${message}`);
148071
+ }
148072
+ const { state, instanceId } = await fleet.resolveOrchestrator(normalizeProjectPath(context.cwd));
148073
+ if (state === "missing" || state === "unresolvable")
148074
+ return null;
148075
+ return typeof instanceId === "string" && instanceId !== "" ? instanceId : null;
148076
+ }
148077
+ function formatInboxLine(entry) {
148078
+ const from = entry.fromId || DEFAULT_FROM;
148079
+ const body = entry.body.length > 60 ? `${entry.body.slice(0, 57)}...` : entry.body;
148080
+ return `${entry.id} ${entry.status} ${from} ${body} ${new Date(entry.createdAt).toISOString()}`;
148081
+ }
148082
+ function widenInboxRow(entry, hold) {
148083
+ return {
148084
+ ...entry,
148085
+ injectAttempts: entry.attempts,
148086
+ injectError: entry.error ?? null,
148087
+ ...hold?.reason !== undefined ? { reason: hold.reason } : {},
148088
+ ...hold?.runId !== undefined ? { runId: hold.runId } : {},
148089
+ taskId: hold?.taskId ?? null,
148090
+ ...hold?.runStatus !== undefined ? { runStatus: hold.runStatus } : {},
148091
+ artifacts: hold?.artifacts ?? []
148092
+ };
148093
+ }
148094
+ async function waitForMessageClaim(context, msgId, agentService, pin, timeoutMs, signal) {
148095
+ const start = Date.now();
148096
+ const deadline = timeoutMs === undefined ? Number.POSITIVE_INFINITY : start + timeoutMs;
148097
+ const stallMs = Math.min(timeoutMs ?? DEFAULT_STALL_MS, DEFAULT_STALL_MS);
148098
+ let lastPending = -1;
148099
+ for (;; ) {
148100
+ if (signal?.aborted)
148101
+ throw new WaitError("timeout", "wait aborted");
148102
+ const clock = Date.now();
148103
+ if (clock >= deadline)
148104
+ throw new WaitError("timeout", `wait timed out after ${timeoutMs}ms`);
148105
+ const occupant = await agentService.getOccupant({ specId: pin.specId });
148106
+ if (occupant === null)
148107
+ throw new WaitError("occupant_gone", `occupant for specId "${pin.specId}" is gone`);
148108
+ if (occupant.runId !== pin.runId)
148109
+ throw new WaitError("run_replaced", `run ${pin.runId} replaced`);
148110
+ if (occupant.generation > pin.generation)
148111
+ throw new WaitError("run_replaced", `generation bumped`);
148112
+ const row = await inboxOf(context).get(msgId);
148113
+ if (row === null)
148114
+ throw new WaitError("occupant_gone", `message ${msgId} disappeared`);
148115
+ const pending = row.status === "queued" ? 1 : 0;
148116
+ const progressed = lastPending >= 0 && pending !== lastPending;
148117
+ if (pending === 0)
148118
+ return;
148119
+ if (lastPending >= 0 && !progressed && clock - start >= stallMs) {
148120
+ throw new WaitError("wait_stalled", `inbox not draining within ${stallMs}ms`);
148121
+ }
148122
+ lastPending = pending;
148123
+ await sendSleep(100, signal);
148124
+ }
148125
+ }
148126
+ function sendSleep(ms, signal) {
148127
+ return new Promise((resolve) => {
148128
+ if (signal?.aborted) {
148129
+ resolve();
148130
+ return;
148131
+ }
148132
+ const timer = setTimeout(resolve, ms);
148133
+ signal?.addEventListener("abort", () => {
148134
+ clearTimeout(timer);
148135
+ resolve();
148136
+ }, { once: true });
148137
+ });
148138
+ }
148139
+ function parseInterval2(raw) {
148140
+ const n = Number.parseInt(raw, 10);
148141
+ if (!Number.isFinite(n) || n <= 0)
148142
+ return null;
148143
+ return n;
148144
+ }
148145
+ function parseTimeout2(raw) {
148146
+ if (raw === undefined || typeof raw === "boolean")
148147
+ return;
148148
+ const n = Number.parseInt(raw, 10);
148149
+ if (!Number.isFinite(n) || n <= 0) {
148150
+ throw new Error(`invalid --timeout "${raw}" (expected a positive integer ms)`);
148151
+ }
148152
+ return n;
148153
+ }
148154
+ function sendWaitFail(context, options, code, message) {
148155
+ if (options.json) {
148156
+ context.output.write(toEnvelopeJson({ error: { code, message } }, {
148157
+ enveloped: options.jsonEnvelope,
148158
+ error: { code: "INTERNAL_ERROR", message, details: { cliCode: code } }
148159
+ }));
148160
+ } else {
148161
+ context.output.error(`${code}: ${message}`);
148162
+ }
148163
+ return 1;
148164
+ }
148165
+ async function readLatestInvokeEvent2(dao, runId) {
148166
+ const rows = await dao.query({
148167
+ run_id: runId,
148168
+ names: ["agent.invoke.start", "agent.invoke.exit"],
148169
+ limit: 1
148170
+ });
148171
+ const row = rows[0];
148172
+ if (row === undefined)
148173
+ return null;
148174
+ return { eventName: row.event_name, sequence: row.sequence };
148175
+ }
148176
+
148177
+ // src/commands/message/inbox.ts
148178
+ function registerMessageInboxCommand(noun, context) {
148179
+ noun.command("inbox").description("List messages addressed to an agent.").requiredOption("--agent <id>", "Agent id").option("--unresolved", "Only messages the 0834 reconciler holds (delivery-failed | attempts-exhausted | outcome-unknown | run-exit-only)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
148180
+ const code = await runMessageInbox(context, options);
148181
+ context.setExitCode(code);
148182
+ });
148183
+ }
148184
+ async function runMessageInbox(context, options) {
148185
+ const classify = options.json === true || options.unresolved === true;
148186
+ let holds = new Map;
148187
+ if (classify) {
148188
+ const reconciler = new DeliveryReconciler(context);
148189
+ holds = new Map((await reconciler.classify(options.agent)).map((u) => [u.messageId, u]));
148190
+ }
148191
+ const page = await inboxOf(context).list(options.agent, "in");
148192
+ const messages = options.unresolved === true ? page.messages.filter((m) => holds.has(m.id)) : page.messages;
148193
+ if (options.json) {
148194
+ context.output.write(toEnvelopeJson({
148195
+ messages: messages.map((m) => widenInboxRow(m, holds.get(m.id))),
148196
+ count: messages.length
148197
+ }, { enveloped: options.jsonEnvelope }));
148198
+ return 0;
148199
+ }
148200
+ if (messages.length === 0) {
148201
+ context.output.write(`No messages for ${options.agent}`);
148202
+ return 0;
148203
+ }
148204
+ context.output.write(messages.map(formatInboxLine).join(`
148205
+ `));
148206
+ return 0;
148207
+ }
148208
+
148209
+ // src/commands/message/read.ts
148210
+ init_src3();
148211
+ function registerMessageReadCommand(noun, context) {
148212
+ noun.command("read").description("Mark a message read by its recipient (the operator by default).").argument("<msg-id>", "Message id").option("--reader <id>", "Reader address", "operator").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (msgId, options) => {
148213
+ const jsonEnvelope = options.jsonEnvelope;
148214
+ context.setExitCode(await runMessageRead(context, msgId, { ...options, jsonEnvelope }));
148215
+ });
148216
+ }
148217
+ async function runMessageRead(context, msgId, options) {
148218
+ try {
148219
+ const receipt = await inboxOf(context).markRead(msgId, options.reader);
148220
+ if (options.json) {
148221
+ context.output.write(toEnvelopeJson(receipt, { enveloped: options.jsonEnvelope }));
148222
+ } else {
148223
+ context.output.write(receipt.transitioned ? `read ${msgId} by ${options.reader}` : `${msgId} was already read by ${options.reader}`);
148224
+ }
148225
+ return 0;
148226
+ } catch (error) {
148227
+ if (!(error instanceof MessageNotFoundError))
148228
+ throw error;
148229
+ writeJsonError(context.output, options, `message ${msgId} not found for ${options.reader}`, "VALIDATION_FAILED");
148230
+ return 1;
148231
+ }
148232
+ }
148233
+
148234
+ // src/commands/message/reply.ts
148235
+ function registerMessageReplyCommand(noun, context) {
148236
+ noun.command("reply").description("Thread a reply to a message.").argument("<msg-id>", "Message id to reply to").argument("<body>", "Reply body").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (msgId, body, options) => {
148237
+ const code = await runMessageReply(context, msgId, body, options);
148238
+ context.setExitCode(code);
148239
+ });
148240
+ }
148241
+ async function runMessageReply(context, msgId, body, options) {
148242
+ const trimmed = body.trim();
148243
+ if (trimmed === "") {
148244
+ writeJsonError(context.output, options, "message reply requires a non-empty body", "VALIDATION_FAILED");
148245
+ return 2;
148246
+ }
148247
+ const inbox = inboxOf(context);
148248
+ const original = await inbox.get(msgId);
148249
+ if (original === null) {
148250
+ writeJsonError(context.output, options, `message ${msgId} not found`, "VALIDATION_FAILED");
148251
+ return 1;
148252
+ }
148253
+ const reply = await inbox.reply(msgId, original.toId, trimmed);
148254
+ if (options.json) {
148255
+ context.output.write(toEnvelopeJson(reply, { enveloped: options.jsonEnvelope }));
148256
+ } else {
148257
+ context.output.write(`replied ${reply.id} \u2192 ${reply.toId}`);
148258
+ }
148259
+ return 0;
148260
+ }
148261
+
148262
+ // src/commands/message/send.ts
148263
+ init_src3();
148264
+ init_src2();
148265
+ init_dist9();
148266
+ var SEND_UNTIL_STATES = new Set(["injected", "invoke-exit"]);
148267
+ function collectSendUntil(value, acc) {
148268
+ if (!SEND_UNTIL_STATES.has(value)) {
148269
+ throw new Error(`invalid --until "${value}" for send --wait (expected one of: ${[...SEND_UNTIL_STATES].join(", ")})`);
148270
+ }
148271
+ return [...acc, value];
148272
+ }
148273
+ function registerMessageSendCommand(noun, context) {
147827
148274
  noun.command("send").description("Enqueue a message for an agent. Use --wait to block until the recipient reaches a state.").argument("<body>", "Message body").option("--to <id>", "Recipient agent id (mutually exclusive with --role)").option("--role <name>", "Address by Layer-1 role or executor name; must resolve to exactly one materialized instance").option("--from <id>", "Sender id", DEFAULT_FROM).option("--request-key <key>", "Caller-minted idempotency key: the same key + payload replays the original receipt instead of enqueuing twice (0832)").option("--wait", "Block until the recipient occupant reaches --until (default: invoke-exit)").option(...SHARED_OPTIONS.untilMessage, collectSendUntil, []).option(...SHARED_OPTIONS.timeout, parseTimeout2).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (body, options) => {
147828
148275
  const svc = new AgentCoordinationService(context);
147829
148276
  let toId = options.to;
@@ -147860,48 +148307,15 @@ function registerMessageCommand(program, context) {
147860
148307
  }
147861
148308
  toId = resolution.specId;
147862
148309
  }
147863
- const code = await runMessageSend(svc, context, body, {
148310
+ const code = await runMessageSend(context, body, {
147864
148311
  ...options,
147865
148312
  to: toId ?? "",
147866
148313
  requestKey: typeof options.requestKey === "string" && options.requestKey.length > 0 ? options.requestKey.trim() : undefined
147867
148314
  });
147868
148315
  context.setExitCode(code);
147869
148316
  });
147870
- noun.command("inbox").description("List messages addressed to an agent.").requiredOption("--agent <id>", "Agent id").option("--unresolved", "Only messages the 0834 reconciler holds (delivery-failed | attempts-exhausted | outcome-unknown | run-exit-only)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
147871
- const svc = new AgentCoordinationService(context);
147872
- const code = await runMessageInbox(svc, context, options);
147873
- context.setExitCode(code);
147874
- });
147875
- noun.command("reply").description("Thread a reply to a message.").argument("<msg-id>", "Message id to reply to").argument("<body>", "Reply body").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (msgId, body, options) => {
147876
- const svc = new AgentCoordinationService(context);
147877
- const code = await runMessageReply(svc, context, msgId, body, options);
147878
- context.setExitCode(code);
147879
- });
147880
- noun.command("watch").description("Follow an agent inbox \u2014 surface new messages as they arrive (Ctrl-C to exit).").requiredOption("--agent <id>", "Agent id to watch").option("--interval <ms>", "Poll interval in milliseconds", String(DEFAULT_WATCH_INTERVAL_MS)).option(...SHARED_OPTIONS.jsonMessageStream).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
147881
- const svc = new AgentCoordinationService(context);
147882
- const intervalMs = parseInterval2(options.interval);
147883
- if (intervalMs === null) {
147884
- writeJsonError(context.output, options, `invalid --interval "${options.interval}" (expected a positive integer ms)`, "VALIDATION_FAILED");
147885
- context.setExitCode(2);
147886
- return;
147887
- }
147888
- const controller = new AbortController;
147889
- const onSigInt = () => controller.abort();
147890
- process.on("SIGINT", onSigInt);
147891
- try {
147892
- await runMessageWatch(svc, context.output, {
147893
- agent: options.agent,
147894
- intervalMs,
147895
- json: options.json ?? false
147896
- }, {
147897
- signal: controller.signal
147898
- });
147899
- } finally {
147900
- process.off("SIGINT", onSigInt);
147901
- }
147902
- });
147903
148317
  }
147904
- async function runMessageSend(svc, context, body, options) {
148318
+ async function runMessageSend(context, body, options) {
147905
148319
  const trimmed = body.trim();
147906
148320
  if (trimmed === "") {
147907
148321
  if (options.json) {
@@ -147918,22 +148332,36 @@ async function runMessageSend(svc, context, body, options) {
147918
148332
  }
147919
148333
  return 2;
147920
148334
  }
148335
+ if (options.requestKey !== undefined && options.requestKey.trim() === "") {
148336
+ writeJsonError(context.output, options, "message send requires a non-empty --request-key", "VALIDATION_FAILED");
148337
+ return 2;
148338
+ }
147921
148339
  if (options.to === "") {
147922
- if (options.json) {
147923
- context.output.write(toEnvelopeJson({ error: { code: "usage", message: "message send requires --to <id> or --role <name>" } }, {
147924
- enveloped: options.jsonEnvelope,
147925
- error: {
147926
- code: "INTERNAL_ERROR",
147927
- message: "message send requires --to <id> or --role <name>",
147928
- details: { cliCode: "usage" }
147929
- }
147930
- }));
147931
- } else {
147932
- context.output.error("message send requires --to <id> or --role <name>");
148340
+ const resolved = await resolveOrchestratorAddress(context);
148341
+ if (resolved === null) {
148342
+ const message = "message send found no orchestrator member \u2014 pass --to <id> or --role <name>, or bind agent.fleet";
148343
+ if (options.json) {
148344
+ context.output.write(toEnvelopeJson({ error: { code: "usage", message } }, {
148345
+ enveloped: options.jsonEnvelope,
148346
+ error: { code: "INTERNAL_ERROR", message, details: { cliCode: "usage" } }
148347
+ }));
148348
+ } else {
148349
+ context.output.error(message);
148350
+ }
148351
+ return 2;
147933
148352
  }
147934
- return 2;
148353
+ options.to = resolved;
147935
148354
  }
147936
148355
  const from = options.from || DEFAULT_FROM;
148356
+ for (const id of [from, options.to]) {
148357
+ try {
148358
+ validateAgentId(id);
148359
+ } catch (error) {
148360
+ const message = error instanceof Error ? error.message : String(error);
148361
+ writeJsonError(context.output, options, message, "VALIDATION_FAILED");
148362
+ return 1;
148363
+ }
148364
+ }
147937
148365
  let pinnedBeforeSend = null;
147938
148366
  if (options.wait === true) {
147939
148367
  const agentService = context.agentService();
@@ -147943,7 +148371,19 @@ async function runMessageSend(svc, context, body, options) {
147943
148371
  }
147944
148372
  pinnedBeforeSend = { specId: options.to, runId: occupant.runId, generation: occupant.generation };
147945
148373
  }
147946
- const result = await svc.sendMessage(from, options.to, trimmed, undefined, options.requestKey);
148374
+ const sent = await inboxOf(context).send({
148375
+ from,
148376
+ to: options.to,
148377
+ body: trimmed,
148378
+ ...options.requestKey !== undefined ? { requestKey: options.requestKey } : {}
148379
+ });
148380
+ const result = {
148381
+ msgId: sent.message.id,
148382
+ toId: sent.message.toId,
148383
+ status: sent.message.status,
148384
+ injected: false,
148385
+ ...options.requestKey !== undefined ? { replayed: sent.replayed, requestKey: options.requestKey } : {}
148386
+ };
147947
148387
  if (options.wait !== true) {
147948
148388
  if (options.json) {
147949
148389
  context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
@@ -147971,7 +148411,7 @@ async function runMessageSend(svc, context, body, options) {
147971
148411
  for (const until of untilList) {
147972
148412
  if (until === "injected") {
147973
148413
  try {
147974
- await waitForPendingDrain(agentService, svc, pin, options.timeout, controller.signal);
148414
+ await waitForMessageClaim(context, result.msgId, agentService, pin, options.timeout, controller.signal);
147975
148415
  satisfied = "injected";
147976
148416
  break;
147977
148417
  } catch (error) {
@@ -147986,7 +148426,7 @@ async function runMessageSend(svc, context, body, options) {
147986
148426
  try {
147987
148427
  const res = await waitForOccupant({
147988
148428
  getOccupant: (id) => agentService.getOccupant({ specId: id }),
147989
- countPending: (id) => svc.countPending(id),
148429
+ countQueued: (id) => inboxOf(context).countQueued(id),
147990
148430
  latestInvokeEvent: (r) => readLatestInvokeEvent2(eventDao, r),
147991
148431
  follow: (afterSequence) => followSystemEventsAfter(context.getDb, {
147992
148432
  afterSequence,
@@ -148029,62 +148469,35 @@ async function runMessageSend(svc, context, body, options) {
148029
148469
  process.off("SIGTERM", onSignal);
148030
148470
  }
148031
148471
  }
148032
- async function runMessageInbox(svc, context, options) {
148033
- const classify = options.json === true || options.unresolved === true;
148034
- let holds = new Map;
148035
- if (classify) {
148036
- const reconciler = new DeliveryReconciler(context);
148037
- const report = await reconciler.reconcile(options.agent);
148038
- holds = new Map(report.unresolved.map((u) => [u.messageId, u]));
148472
+
148473
+ // src/commands/message/show.ts
148474
+ function registerMessageShowCommand(noun, context) {
148475
+ noun.command("show").description("Print one message thread: the root and every reply, oldest first, both directions.").argument("<msg-id>", "Message id (or any id in its thread)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (msgId, options) => {
148476
+ context.setExitCode(await runMessageShow(context, msgId, { json: options.json, jsonEnvelope: options.jsonEnvelope }));
148477
+ });
148478
+ }
148479
+ async function runMessageShow(context, msgId, options) {
148480
+ const rows = await inboxOf(context).thread(msgId);
148481
+ if (rows.length === 0) {
148482
+ writeJsonError(context.output, options, `message ${msgId} not found`, "VALIDATION_FAILED");
148483
+ return 1;
148039
148484
  }
148040
- const inbox = await svc.getInbox(options.agent);
148041
- const messages = options.unresolved === true ? inbox.messages.filter((m) => holds.has(m.id)) : inbox.messages;
148042
148485
  if (options.json) {
148043
- context.output.write(toEnvelopeJson({
148044
- messages: messages.map((m) => widenInboxRow(m, holds.get(m.id))),
148045
- count: messages.length
148046
- }, { enveloped: options.jsonEnvelope }));
148486
+ context.output.write(toEnvelopeJson({ messages: rows, count: rows.length }, { enveloped: options.jsonEnvelope }));
148047
148487
  return 0;
148048
148488
  }
148049
- if (messages.length === 0) {
148050
- context.output.write(`No messages for ${options.agent}`);
148051
- return 0;
148052
- }
148053
- context.output.write(messages.map(formatInboxLine).join(`
148489
+ context.output.write(rows.map((row) => {
148490
+ const from = row.fromId || DEFAULT_FROM;
148491
+ const read = row.readAt === null ? "" : " (read)";
148492
+ return `${new Date(row.createdAt).toISOString()} ${from} \u2192 ${row.toId} ${row.status}${read}
148493
+ ${row.body}`;
148494
+ }).join(`
148495
+
148054
148496
  `));
148055
148497
  return 0;
148056
148498
  }
148057
- function widenInboxRow(entry, hold) {
148058
- return {
148059
- ...entry,
148060
- injectAttempts: entry.injectAttempts,
148061
- injectError: entry.injectError,
148062
- reason: hold?.reason,
148063
- runId: hold?.runId,
148064
- taskId: hold?.taskId,
148065
- runStatus: hold?.runStatus,
148066
- artifacts: hold?.artifacts ?? []
148067
- };
148068
- }
148069
- async function runMessageReply(svc, context, msgId, body, options) {
148070
- const trimmed = body.trim();
148071
- if (trimmed === "") {
148072
- writeJsonError(context.output, options, "message reply requires a non-empty body", "VALIDATION_FAILED");
148073
- return 2;
148074
- }
148075
- const result = await svc.replyToMessage(msgId, trimmed);
148076
- if (options.json) {
148077
- context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
148078
- } else {
148079
- context.output.write(`replied ${result.msgId} \u2192 ${result.toId}`);
148080
- }
148081
- return 0;
148082
- }
148083
- function formatInboxLine(entry) {
148084
- const from = entry.fromId ?? DEFAULT_FROM;
148085
- const body = entry.body.length > 60 ? `${entry.body.slice(0, 57)}...` : entry.body;
148086
- return `${entry.id} ${entry.status} ${from} ${body} ${entry.createdAt}`;
148087
- }
148499
+
148500
+ // src/commands/message/watch.ts
148088
148501
  async function runMessageWatch(svc, output, options, runtime = {}) {
148089
148502
  const sleep = runtime.sleep ?? ((ms) => defaultSleep(ms, runtime.signal));
148090
148503
  const seen = new Set;
@@ -148094,9 +148507,9 @@ async function runMessageWatch(svc, output, options, runtime = {}) {
148094
148507
  return;
148095
148508
  if (runtime.maxIterations !== undefined && iteration >= runtime.maxIterations)
148096
148509
  return;
148097
- const inbox = await svc.getInbox(options.agent);
148098
- for (let i = inbox.messages.length - 1;i >= 0; i--) {
148099
- const msg = inbox.messages[i];
148510
+ const page = await svc.list(options.agent, "in");
148511
+ for (let i = page.messages.length - 1;i >= 0; i--) {
148512
+ const msg = page.messages[i];
148100
148513
  if (!msg || seen.has(msg.id))
148101
148514
  continue;
148102
148515
  seen.add(msg.id);
@@ -148123,91 +148536,40 @@ function defaultSleep(ms, signal) {
148123
148536
  }, { once: true });
148124
148537
  });
148125
148538
  }
148126
- function parseInterval2(raw) {
148127
- const n = Number.parseInt(raw, 10);
148128
- if (!Number.isFinite(n) || n <= 0)
148129
- return null;
148130
- return n;
148131
- }
148132
- var SEND_UNTIL_STATES = new Set(["injected", "invoke-exit"]);
148133
- function collectSendUntil(value, acc) {
148134
- if (!SEND_UNTIL_STATES.has(value)) {
148135
- throw new Error(`invalid --until "${value}" for send --wait (expected one of: ${[...SEND_UNTIL_STATES].join(", ")})`);
148136
- }
148137
- return [...acc, value];
148138
- }
148139
- function parseTimeout2(raw) {
148140
- if (raw === undefined || typeof raw === "boolean")
148141
- return;
148142
- const n = Number.parseInt(raw, 10);
148143
- if (!Number.isFinite(n) || n <= 0) {
148144
- throw new Error(`invalid --timeout "${raw}" (expected a positive integer ms)`);
148145
- }
148146
- return n;
148147
- }
148148
- function sendSleep(ms, signal) {
148149
- return new Promise((resolve) => {
148150
- if (signal?.aborted) {
148151
- resolve();
148539
+ function registerMessageWatchCommand(noun, context) {
148540
+ noun.command("watch").description("Follow an agent inbox \u2014 surface new messages as they arrive (Ctrl-C to exit).").requiredOption("--agent <id>", "Agent id to watch").option("--interval <ms>", "Poll interval in milliseconds", String(DEFAULT_WATCH_INTERVAL_MS)).option(...SHARED_OPTIONS.jsonMessageStream).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
148541
+ const intervalMs = parseInterval2(options.interval);
148542
+ if (intervalMs === null) {
148543
+ writeJsonError(context.output, options, `invalid --interval "${options.interval}" (expected a positive integer ms)`, "VALIDATION_FAILED");
148544
+ context.setExitCode(2);
148152
148545
  return;
148153
148546
  }
148154
- const timer = setTimeout(resolve, ms);
148155
- signal?.addEventListener("abort", () => {
148156
- clearTimeout(timer);
148157
- resolve();
148158
- }, { once: true });
148159
- });
148160
- }
148161
- function sendWaitFail(context, options, code, message) {
148162
- if (options.json) {
148163
- context.output.write(toEnvelopeJson({ error: { code, message } }, {
148164
- enveloped: options.jsonEnvelope,
148165
- error: { code: "INTERNAL_ERROR", message, details: { cliCode: code } }
148166
- }));
148167
- } else {
148168
- context.output.error(`${code}: ${message}`);
148169
- }
148170
- return 1;
148171
- }
148172
- async function readLatestInvokeEvent2(dao, runId) {
148173
- const rows = await dao.query({
148174
- run_id: runId,
148175
- names: ["agent.invoke.start", "agent.invoke.exit"],
148176
- limit: 1
148547
+ const controller = new AbortController;
148548
+ const onSigInt = () => controller.abort();
148549
+ process.on("SIGINT", onSigInt);
148550
+ try {
148551
+ await runMessageWatch(inboxOf(context), context.output, {
148552
+ agent: options.agent,
148553
+ intervalMs,
148554
+ json: options.json ?? false
148555
+ }, {
148556
+ signal: controller.signal
148557
+ });
148558
+ } finally {
148559
+ process.off("SIGINT", onSigInt);
148560
+ }
148177
148561
  });
148178
- const row = rows[0];
148179
- if (row === undefined)
148180
- return null;
148181
- return { eventName: row.event_name, sequence: row.sequence };
148182
148562
  }
148183
- async function waitForPendingDrain(agentService, coordination, pin, timeoutMs, signal) {
148184
- const start = Date.now();
148185
- const deadline = timeoutMs === undefined ? Number.POSITIVE_INFINITY : start + timeoutMs;
148186
- const stallMs = Math.min(timeoutMs ?? DEFAULT_STALL_MS, DEFAULT_STALL_MS);
148187
- let lastPending = -1;
148188
- for (;; ) {
148189
- if (signal?.aborted)
148190
- throw new WaitError("timeout", "wait aborted");
148191
- const clock = Date.now();
148192
- if (clock >= deadline)
148193
- throw new WaitError("timeout", `wait timed out after ${timeoutMs}ms`);
148194
- const occupant = await agentService.getOccupant({ specId: pin.specId });
148195
- if (occupant === null)
148196
- throw new WaitError("occupant_gone", `occupant for specId "${pin.specId}" is gone`);
148197
- if (occupant.runId !== pin.runId)
148198
- throw new WaitError("run_replaced", `run ${pin.runId} replaced`);
148199
- if (occupant.generation > pin.generation)
148200
- throw new WaitError("run_replaced", `generation bumped`);
148201
- const pending = await coordination.countPending(pin.specId);
148202
- const progressed = lastPending >= 0 && pending !== lastPending;
148203
- if (pending === 0)
148204
- return;
148205
- if (lastPending >= 0 && !progressed && clock - start >= stallMs) {
148206
- throw new WaitError("wait_stalled", `inbox not draining within ${stallMs}ms`);
148207
- }
148208
- lastPending = pending;
148209
- await sendSleep(100, signal);
148210
- }
148563
+
148564
+ // src/commands/message/index.ts
148565
+ function registerMessageCommand(program, context) {
148566
+ const noun = program.command("message").summary("send and inspect durable inter-agent messages");
148567
+ registerMessageSendCommand(noun, context);
148568
+ registerMessageInboxCommand(noun, context);
148569
+ registerMessageShowCommand(noun, context);
148570
+ registerMessageReadCommand(noun, context);
148571
+ registerMessageReplyCommand(noun, context);
148572
+ registerMessageWatchCommand(noun, context);
148211
148573
  }
148212
148574
 
148213
148575
  // src/commands/migrate.ts
@@ -154915,6 +155277,134 @@ var historyContract = {
154915
155277
  }).input(historyTriggerImportInputSchema).output(historyTriggerImportResponseSchema)
154916
155278
  };
154917
155279
 
155280
+ // ../../packages/contracts/src/inbox.ts
155281
+ init_zod();
155282
+ var INBOX_MESSAGE_KINDS = ["chat", "request"];
155283
+ var INBOX_MESSAGE_STATUSES = ["queued", "claimed", "delivered", "failed"];
155284
+ var inboxMessageSchema = object2({
155285
+ id: string2(),
155286
+ fromId: string2(),
155287
+ toId: string2(),
155288
+ body: string2(),
155289
+ kind: _enum2(INBOX_MESSAGE_KINDS),
155290
+ status: _enum2(INBOX_MESSAGE_STATUSES),
155291
+ attempts: number2().int().nonnegative(),
155292
+ error: string2().nullable(),
155293
+ claimedAt: number2().int().nullable(),
155294
+ deliveredAt: number2().int().nullable(),
155295
+ readAt: number2().int().nullable(),
155296
+ inReplyTo: string2().nullable(),
155297
+ threadId: string2(),
155298
+ requestKey: string2().nullable(),
155299
+ taskId: string2().nullable(),
155300
+ parentRunId: string2().nullable(),
155301
+ createdAt: number2().int()
155302
+ });
155303
+ var inboxMemberSchema = object2({
155304
+ id: string2(),
155305
+ queued: number2().int().nonnegative(),
155306
+ unread: number2().int().nonnegative()
155307
+ });
155308
+ var inboxRequestSchema = object2({
155309
+ messageId: string2(),
155310
+ requestKey: string2().nullable(),
155311
+ status: _enum2(INBOX_MESSAGE_STATUSES),
155312
+ attempts: number2().int().nonnegative(),
155313
+ error: string2().nullable(),
155314
+ runId: string2().nullable(),
155315
+ taskId: string2().nullable(),
155316
+ outcome: string2().nullable(),
155317
+ reason: string2().nullable(),
155318
+ hold: string2().nullable()
155319
+ });
155320
+ var inboxListInputSchema = object2({
155321
+ member: string2().min(1),
155322
+ direction: _enum2(["in", "out", "both"]).optional(),
155323
+ limit: number2().int().positive().max(500).optional(),
155324
+ cursor: string2().optional()
155325
+ });
155326
+ var inboxListResponseSchema = object2({
155327
+ messages: array(inboxMessageSchema),
155328
+ nextCursor: string2().nullable()
155329
+ });
155330
+ var inboxThreadInputSchema = object2({
155331
+ id: string2().min(1)
155332
+ });
155333
+ var inboxSendInputSchema = object2({
155334
+ to: string2().min(1).optional(),
155335
+ from: string2().min(1).optional(),
155336
+ body: string2().min(1),
155337
+ kind: _enum2(INBOX_MESSAGE_KINDS).optional(),
155338
+ requestKey: string2().min(1).optional(),
155339
+ taskId: string2().min(1).optional(),
155340
+ parentRunId: string2().min(1).optional(),
155341
+ projectPath: string2().optional()
155342
+ });
155343
+ var inboxSendResponseSchema = object2({
155344
+ message: inboxMessageSchema,
155345
+ replayed: boolean2()
155346
+ });
155347
+ var inboxReplyInputSchema = object2({
155348
+ id: string2().min(1),
155349
+ from: string2().min(1),
155350
+ body: string2().min(1)
155351
+ });
155352
+ var inboxReadInputSchema = object2({
155353
+ id: string2().min(1),
155354
+ reader: string2().min(1)
155355
+ });
155356
+ var inboxReadResponseSchema = object2({
155357
+ readAt: number2().int(),
155358
+ transitioned: boolean2()
155359
+ });
155360
+ var inboxRequestsInputSchema = object2({
155361
+ limit: number2().int().positive().max(200).optional()
155362
+ });
155363
+ var inboxContract = {
155364
+ list: oc.route({
155365
+ method: "GET",
155366
+ path: "/inbox/messages",
155367
+ summary: "List one inbox address messages",
155368
+ tags: ["inbox"]
155369
+ }).input(inboxListInputSchema).output(inboxListResponseSchema),
155370
+ thread: oc.route({
155371
+ method: "GET",
155372
+ path: "/inbox/messages/{id}/thread",
155373
+ summary: "Read one message thread (root plus replies, oldest first)",
155374
+ tags: ["inbox"]
155375
+ }).input(inboxThreadInputSchema).output(array(inboxMessageSchema)),
155376
+ send: oc.route({
155377
+ method: "POST",
155378
+ path: "/inbox/messages",
155379
+ summary: "Send a message to a member inbox",
155380
+ tags: ["inbox"]
155381
+ }).input(inboxSendInputSchema).output(inboxSendResponseSchema),
155382
+ reply: oc.route({
155383
+ method: "POST",
155384
+ path: "/inbox/messages/{id}/reply",
155385
+ summary: "Reply in a thread, addressed to the original sender",
155386
+ tags: ["inbox"]
155387
+ }).input(inboxReplyInputSchema).output(inboxMessageSchema),
155388
+ read: oc.route({
155389
+ method: "POST",
155390
+ path: "/inbox/messages/{id}/read",
155391
+ summary: "Mark a message read by its recipient",
155392
+ tags: ["inbox"]
155393
+ }).input(inboxReadInputSchema).output(inboxReadResponseSchema),
155394
+ requests: oc.route({
155395
+ method: "GET",
155396
+ path: "/inbox/requests",
155397
+ summary: "List request results with delivery classification",
155398
+ tags: ["inbox"]
155399
+ }).input(inboxRequestsInputSchema).output(array(inboxRequestSchema)),
155400
+ members: oc.route({
155401
+ method: "GET",
155402
+ path: "/inbox/members",
155403
+ summary: "List inbox addresses with queued and unread counts",
155404
+ tags: ["inbox"]
155405
+ }).input(object2({})).output(array(inboxMemberSchema))
155406
+ };
155407
+
154918
155408
  // ../../packages/contracts/src/planning-event.ts
154919
155409
  init_zod();
154920
155410
  var planningEventEnvelopeSchema = apiSuccessSchema(object2({
@@ -155351,6 +155841,7 @@ var contract = {
155351
155841
  board: { ...boardContract },
155352
155842
  feature: { ...featureContract },
155353
155843
  history: { ...historyContract },
155844
+ inbox: { ...inboxContract },
155354
155845
  fleet: { ...fleetContract },
155355
155846
  processes: { ...processesContract },
155356
155847
  runs: { ...runsContract },
@@ -156344,6 +156835,9 @@ var databaseModule = {
156344
156835
  }
156345
156836
  };
156346
156837
 
156838
+ // ../server/src/modules/events/index.ts
156839
+ init_src3();
156840
+
156347
156841
  // ../server/src/modules/sse/stream-helpers.ts
156348
156842
  function sendSseKeepalive(closed, controller, encoder) {
156349
156843
  if (closed.current)
@@ -156436,6 +156930,34 @@ function extractSystemEventActor2(event) {
156436
156930
  function sendKeepalive(closed, controller, encoder) {
156437
156931
  sendSseKeepalive(closed, controller, encoder);
156438
156932
  }
156933
+ var relayedContexts = new WeakSet;
156934
+ var relayedRowColumns = new WeakMap;
156935
+ function startMessageRelay(ctx) {
156936
+ if (relayedContexts.has(ctx))
156937
+ return;
156938
+ relayedContexts.add(ctx);
156939
+ const controller = new AbortController;
156940
+ (async () => {
156941
+ try {
156942
+ const bus = ctx.eventBus();
156943
+ const start = await (await ctx.systemEventDao()).latestSequence();
156944
+ const stream = followSystemEventsAfter(() => ctx.getDb(), {
156945
+ afterSequence: start,
156946
+ match: (row) => row.event_name.startsWith("message."),
156947
+ signal: controller.signal
156948
+ });
156949
+ for await (const row of stream) {
156950
+ try {
156951
+ const payload = JSON.parse(row.payload_json ?? "{}");
156952
+ if (payload !== null && typeof payload === "object") {
156953
+ relayedRowColumns.set(payload, { actor: row.actor, occurredAt: row.occurred_at });
156954
+ }
156955
+ bus.emit(row.event_name, payload);
156956
+ } catch {}
156957
+ }
156958
+ } catch {}
156959
+ })();
156960
+ }
156439
156961
  var eventsModule = {
156440
156962
  name: "events",
156441
156963
  mount(app, ctx) {
@@ -156447,6 +156969,7 @@ var eventsModule = {
156447
156969
  const diagnosticEnabled = typeof ctx.bootConfig === "function" && ctx.bootConfig().events.diagnostic === true;
156448
156970
  const diagnosticNames = diagnosticEnabled ? SYSTEM_EVENT_CATALOG.filter((entry) => entry.tier === "diagnostic" && entry.streamed).map((entry) => entry.name) : [];
156449
156971
  const streamNames = [...baseNames, ...diagnosticNames];
156972
+ startMessageRelay(ctx);
156450
156973
  app.get("/api/events/planning", (c) => {
156451
156974
  const bus = ctx.eventBus();
156452
156975
  const closed = { current: false };
@@ -156485,14 +157008,15 @@ var eventsModule = {
156485
157008
  if (closed.current)
156486
157009
  return;
156487
157010
  const entry = systemEventCatalogEntry(name);
156488
- const actor = extractSystemEventActor2(event);
157011
+ const ledger = event !== null && typeof event === "object" ? relayedRowColumns.get(event) : undefined;
157012
+ const actor = ledger !== undefined ? ledger.actor : extractSystemEventActor2(event);
156489
157013
  const envelope = {
156490
157014
  eventName: name,
156491
- occurredAt: new Date().toISOString(),
157015
+ occurredAt: ledger?.occurredAt ?? new Date().toISOString(),
156492
157016
  actor,
156493
157017
  prefix: entry?.prefix ?? name.split(".")[0],
156494
157018
  renderer: entry?.renderer ?? "generic",
156495
- payload: buildSystemEventEnvelope(entry, event, projectContext, secretValues, actor)
157019
+ payload: event !== null && typeof event === "object" && "schemaVersion" in event ? projectStoredSystemEventEnvelope(entry, event, projectContext, secretValues, actor) : buildSystemEventEnvelope(entry, event, projectContext, secretValues, actor)
156496
157020
  };
156497
157021
  try {
156498
157022
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(envelope)}
@@ -156730,11 +157254,10 @@ init_dist5();
156730
157254
  import { homedir as homedir17 } from "os";
156731
157255
  import { basename as basename20, join as join94, relative as relative19, resolve as resolve37 } from "path";
156732
157256
  // ../server/package.json
156733
- var version3 = "0.4.17";
157257
+ var version3 = "0.4.19";
156734
157258
 
156735
157259
  // ../server/src/modules/health/index.ts
156736
157260
  var startedAt = Date.now();
156737
- var OPERATOR_AGENT_ID = "board-operator";
156738
157261
  var healthModule = {
156739
157262
  name: "health",
156740
157263
  mount(app, ctx) {
@@ -157003,77 +157526,6 @@ var healthModule = {
157003
157526
  executors
157004
157527
  });
157005
157528
  });
157006
- app.get("/api/project/requests", async (c) => {
157007
- if (!ctx) {
157008
- return c.json({ requests: [] });
157009
- }
157010
- const limitRaw = c.req.query("limit");
157011
- const parsedLimit = limitRaw === undefined ? NaN : Number.parseInt(limitRaw, 10);
157012
- const limit = Number.isNaN(parsedLimit) || parsedLimit < 0 ? 50 : Math.min(parsedLimit, 200);
157013
- const path = normalizeProjectPath(ctx.cwd);
157014
- const openDb = () => ctx.getDb();
157015
- const fleet = new FleetService({
157016
- fs: ctx.fs,
157017
- reloadAgentConfig: () => ctx.reloadAgentConfig(),
157018
- openDb
157019
- });
157020
- const runtime = new StrategyRuntime({
157021
- openDb,
157022
- tasks: ctx.taskService(),
157023
- fleet,
157024
- dependencyBlocked: async () => null
157025
- });
157026
- let orchestrator = { state: "unresolvable", reason: "unavailable" };
157027
- try {
157028
- const { state, instanceId, holderId, reason } = await fleet.resolveOrchestrator(path);
157029
- orchestrator = { state, instanceId, holderId, reason };
157030
- } catch {
157031
- orchestrator = { state: "unresolvable", reason: "unavailable" };
157032
- }
157033
- const instanceId = orchestrator.state === "missing" || orchestrator.state === "unresolvable" ? undefined : orchestrator.instanceId;
157034
- if (instanceId === undefined) {
157035
- return c.json({ requests: [] });
157036
- }
157037
- const db = await openDb();
157038
- const unresolved = await new DeliveryReconciler({ getDb: openDb }).classify(instanceId);
157039
- try {
157040
- await new FleetGuestService({
157041
- cwd: ctx.cwd,
157042
- fs: ctx.fs ?? createNodeFileSystem2(ctx.cwd),
157043
- getDb: openDb
157044
- }).expire();
157045
- } catch {}
157046
- const reasonByMessage = new Map(unresolved.map((u) => [u.messageId, u.reason]));
157047
- const runs = new CoordinationRunDao(db);
157048
- const holdByTask = new Map;
157049
- try {
157050
- for (const h of (await runtime.selectNext(path, { readOnly: true })).holds) {
157051
- holdByTask.set(h.wbs, h.reason);
157052
- }
157053
- } catch {}
157054
- const REQUESTED_RUN_OUTCOMES = ["run-exit-only", "errored", "verified"];
157055
- const requests = [];
157056
- for (const row of await new InboxMessageDao(db).inbox(instanceId, 200)) {
157057
- if (row.fromId !== OPERATOR_AGENT_ID)
157058
- continue;
157059
- if (requests.length >= limit)
157060
- break;
157061
- const runRow = (await runs.listByMessageId(row.id))[0];
157062
- requests.push({
157063
- messageId: row.id,
157064
- requestKey: row.requestKey,
157065
- deliveryStatus: row.status,
157066
- injectAttempts: row.injectAttempts,
157067
- injectError: row.injectError,
157068
- runId: runRow?.run_id ?? null,
157069
- taskId: runRow?.task_id ?? null,
157070
- outcome: runRow !== undefined && REQUESTED_RUN_OUTCOMES.includes(runRow.outcome) ? runRow.outcome : null,
157071
- reason: reasonByMessage.get(row.id) ?? null,
157072
- hold: runRow?.task_id !== null && runRow?.task_id !== undefined ? holdByTask.get(runRow.task_id) ?? null : null
157073
- });
157074
- }
157075
- return c.json({ requests });
157076
- });
157077
157529
  app.get("/api/projects", async (c) => {
157078
157530
  if (!ctx) {
157079
157531
  return c.json({ projects: [] });
@@ -157735,130 +158187,6 @@ var jobsModule = {
157735
158187
  }
157736
158188
  };
157737
158189
 
157738
- // ../server/src/modules/messages/index.ts
157739
- init_src3();
157740
- var messagesModule = {
157741
- name: "messages",
157742
- mount(app, ctx) {
157743
- if (!ctx)
157744
- return;
157745
- app.get("/api/messages/inbox", async (c) => {
157746
- const agent = c.req.query("agent");
157747
- if (!agent) {
157748
- return c.json({ error: "missing required query parameter: agent" }, 400);
157749
- }
157750
- const limit = parseLimit(c.req.query("limit"), 50);
157751
- const offset = parseOffset(c.req.query("offset"));
157752
- const svc = ctx.coordination();
157753
- const result = await svc.getInbox(agent, limit, offset);
157754
- return c.json(result);
157755
- });
157756
- app.get("/api/messages", async (c) => {
157757
- const limit = parseLimit(c.req.query("limit"), 50);
157758
- const svc = ctx.coordination();
157759
- const result = await svc.listRecent(limit);
157760
- return c.json(result);
157761
- });
157762
- app.post("/api/messages", async (c) => {
157763
- const parsed = await parseJsonBody(c);
157764
- if ("error" in parsed)
157765
- return c.json({ error: parsed.error }, 400);
157766
- const { to, body, from, requestKey, projectPath } = parsed;
157767
- if (typeof to !== "string" || to.length === 0)
157768
- return c.json({ error: 'field "to" is required' }, 400);
157769
- if (typeof body !== "string" || body.length === 0)
157770
- return c.json({ error: 'field "body" is required' }, 400);
157771
- if (from !== undefined && (typeof from !== "string" || from.length === 0)) {
157772
- return c.json({ error: 'field "from" must be a non-empty string when present' }, 400);
157773
- }
157774
- if (requestKey !== undefined && (typeof requestKey !== "string" || requestKey.length === 0)) {
157775
- return c.json({ error: 'field "requestKey" must be a non-empty string when present' }, 400);
157776
- }
157777
- if (projectPath !== undefined && (typeof projectPath !== "string" || projectPath.length === 0)) {
157778
- return c.json({ error: 'field "projectPath" must be a non-empty string when present' }, 400);
157779
- }
157780
- if (typeof projectPath === "string" && projectPath.length > 0) {
157781
- const served = normalizeProjectPath(ctx.cwd);
157782
- const claimed = normalizeProjectPath(projectPath);
157783
- if (claimed !== served) {
157784
- return c.json({ error: `project mismatch: request targets ${claimed}, server serves ${served}` }, 409);
157785
- }
157786
- }
157787
- const svc = ctx.coordination();
157788
- try {
157789
- const result = await svc.sendMessage(from ?? null, to, body, undefined, requestKey);
157790
- return c.json(result, 201);
157791
- } catch (err) {
157792
- return c.json({ error: errMsg(err) }, 400);
157793
- }
157794
- });
157795
- app.post("/api/messages/:id/reply", async (c) => {
157796
- const id = c.req.param("id");
157797
- const parsed = await parseJsonBody(c);
157798
- if ("error" in parsed)
157799
- return c.json({ error: parsed.error }, 400);
157800
- const { body } = parsed;
157801
- if (typeof body !== "string" || body.length === 0)
157802
- return c.json({ error: 'field "body" is required' }, 400);
157803
- const svc = ctx.coordination();
157804
- try {
157805
- const result = await svc.replyToMessage(id, body);
157806
- return c.json(result, 201);
157807
- } catch (err) {
157808
- return c.json({ error: errMsg(err) }, 400);
157809
- }
157810
- });
157811
- app.post("/api/messages/:id/read", async (c) => {
157812
- const id = c.req.param("id");
157813
- const parsed = await parseJsonBody(c);
157814
- if ("error" in parsed)
157815
- return c.json({ error: parsed.error }, 400);
157816
- const { reader } = parsed;
157817
- if (typeof reader !== "string" || reader.length === 0)
157818
- return c.json({ error: 'field "reader" is required' }, 400);
157819
- try {
157820
- return c.json(await ctx.coordination().markMessageRead(id, reader));
157821
- } catch (err) {
157822
- return c.json({ error: errMsg(err) }, err instanceof MessageNotFoundError ? 404 : 400);
157823
- }
157824
- });
157825
- }
157826
- };
157827
- async function parseJsonBody(c) {
157828
- let json;
157829
- try {
157830
- json = await c.req.json();
157831
- } catch {
157832
- return { error: "request body must be valid JSON" };
157833
- }
157834
- if (typeof json !== "object" || json === null || Array.isArray(json)) {
157835
- return { error: "request body must be a JSON object" };
157836
- }
157837
- return json;
157838
- }
157839
- function errMsg(err) {
157840
- if (err instanceof Error)
157841
- return err.message;
157842
- return String(err);
157843
- }
157844
- function parseLimit(raw, fallback) {
157845
- if (raw === undefined)
157846
- return fallback;
157847
- const parsed = Number.parseInt(raw, 10);
157848
- if (Number.isNaN(parsed) || parsed < 0)
157849
- return fallback;
157850
- return Math.min(parsed, 500);
157851
- }
157852
- function parseOffset(raw) {
157853
- if (raw === undefined)
157854
- return;
157855
- const trimmed = raw.trim();
157856
- if (!/^[0-9]+$/.test(trimmed))
157857
- return 0;
157858
- const parsed = Number(trimmed);
157859
- return Number.isSafeInteger(parsed) ? parsed : 0;
157860
- }
157861
-
157862
158190
  // ../server/src/modules/observability/index.ts
157863
158191
  init_src3();
157864
158192
  init_src2();
@@ -158563,7 +158891,6 @@ var builtins = [
158563
158891
  taskModule,
158564
158892
  featureModule,
158565
158893
  eventsModule,
158566
- messagesModule,
158567
158894
  jobsModule,
158568
158895
  processesModule,
158569
158896
  observabilityModule,
@@ -159889,8 +160216,207 @@ async function generateOpenApiSpec(pluginPaths = {}) {
159889
160216
  return spec;
159890
160217
  }
159891
160218
 
159892
- // ../server/src/router.ts
160219
+ // ../server/src/modules/inbox/handlers.ts
160220
+ init_src3();
159893
160221
  var os5 = implement(contract);
160222
+ function toMessageDto(row) {
160223
+ return {
160224
+ id: row.id,
160225
+ fromId: row.fromId,
160226
+ toId: row.toId,
160227
+ body: row.body,
160228
+ kind: row.kind,
160229
+ status: row.status,
160230
+ attempts: row.attempts,
160231
+ error: row.error,
160232
+ claimedAt: row.claimedAt,
160233
+ deliveredAt: row.deliveredAt,
160234
+ readAt: row.readAt,
160235
+ inReplyTo: row.inReplyTo,
160236
+ threadId: row.threadId,
160237
+ requestKey: row.requestKey,
160238
+ taskId: row.taskId,
160239
+ parentRunId: row.parentRunId,
160240
+ createdAt: row.createdAt
160241
+ };
160242
+ }
160243
+ function toOrpcError(error, fallback) {
160244
+ if (error instanceof MessageNotFoundError)
160245
+ return new ORPCError("NOT_FOUND", { message: error.message });
160246
+ if (error instanceof RequestKeyConflictError2)
160247
+ return new ORPCError("CONFLICT", { message: error.message });
160248
+ const message = error instanceof Error ? error.message : String(error);
160249
+ if (message.startsWith("inbox message requires") || message.includes("must be a non-empty")) {
160250
+ return new ORPCError("BAD_REQUEST", { message });
160251
+ }
160252
+ if (message.includes("malformed list cursor")) {
160253
+ return new ORPCError("BAD_REQUEST", { message: "the list cursor is malformed" });
160254
+ }
160255
+ if (message.includes("already names a different message"))
160256
+ return new ORPCError("CONFLICT", { message });
160257
+ return new ORPCError("INTERNAL_SERVER_ERROR", { message: "the inbox request failed" });
160258
+ }
160259
+ function assertServedProject(ctx, projectPath) {
160260
+ if (projectPath === undefined || projectPath === "")
160261
+ return;
160262
+ const served = normalizeProjectPath(ctx.cwd);
160263
+ const claimed = normalizeProjectPath(projectPath);
160264
+ if (claimed !== served) {
160265
+ throw new ORPCError("CONFLICT", {
160266
+ message: `project mismatch: request targets ${claimed}, server serves ${served}`
160267
+ });
160268
+ }
160269
+ }
160270
+ function fleetOver(ctx) {
160271
+ const injected = ctx.fleetService;
160272
+ if (typeof injected === "function")
160273
+ return injected.call(ctx);
160274
+ return new FleetService({
160275
+ fs: ctx.fs,
160276
+ reloadAgentConfig: () => ctx.reloadAgentConfig(),
160277
+ openDb: () => ctx.getDb()
160278
+ });
160279
+ }
160280
+ async function addressableRecipients(ctx) {
160281
+ const addresses = new Set([OPERATOR_INBOX_ID, SPUR_INBOX_ID]);
160282
+ try {
160283
+ for (const spec of await ctx.coordination().listAgentSpecs())
160284
+ addresses.add(spec.id);
160285
+ } catch {}
160286
+ try {
160287
+ for (const member of await new FleetGuestService({
160288
+ cwd: ctx.cwd,
160289
+ fs: ctx.fs,
160290
+ getDb: () => ctx.getDb()
160291
+ }).list()) {
160292
+ addresses.add(member.id);
160293
+ }
160294
+ } catch {}
160295
+ try {
160296
+ for (const member of await ctx.inbox().members())
160297
+ addresses.add(member.id);
160298
+ } catch {}
160299
+ return addresses;
160300
+ }
160301
+ async function orchestratorAddress(ctx) {
160302
+ try {
160303
+ const { state, instanceId } = await fleetOver(ctx).resolveOrchestrator(normalizeProjectPath(ctx.cwd));
160304
+ if (state === "missing" || state === "unresolvable")
160305
+ return "";
160306
+ return typeof instanceId === "string" ? instanceId : "";
160307
+ } catch {
160308
+ return "";
160309
+ }
160310
+ }
160311
+ function createInboxHandlers(ctx) {
160312
+ return {
160313
+ list: os5.inbox.list.handler(async ({ input }) => {
160314
+ try {
160315
+ const page = await ctx.inbox().list(input.member, input.direction ?? "in", {
160316
+ ...input.limit !== undefined ? { limit: input.limit } : {},
160317
+ ...input.cursor !== undefined ? { cursor: input.cursor } : {}
160318
+ });
160319
+ return { messages: page.messages.map(toMessageDto), nextCursor: page.nextCursor };
160320
+ } catch (error) {
160321
+ throw toOrpcError(error, "BAD_REQUEST");
160322
+ }
160323
+ }),
160324
+ thread: os5.inbox.thread.handler(async ({ input }) => {
160325
+ const rows = await ctx.inbox().thread(input.id);
160326
+ if (rows.length === 0)
160327
+ throw new ORPCError("NOT_FOUND", { message: `no message ${input.id}` });
160328
+ return rows.map(toMessageDto);
160329
+ }),
160330
+ send: os5.inbox.send.handler(async ({ input }) => {
160331
+ assertServedProject(ctx, input.projectPath);
160332
+ let to = input.to;
160333
+ if (to === undefined) {
160334
+ to = await orchestratorAddress(ctx);
160335
+ if (to === "") {
160336
+ throw new ORPCError("BAD_REQUEST", {
160337
+ message: "no orchestrator member is bound for this project \u2014 pass `to` explicitly"
160338
+ });
160339
+ }
160340
+ }
160341
+ if (!(await addressableRecipients(ctx)).has(to)) {
160342
+ throw new ORPCError("BAD_REQUEST", {
160343
+ message: `unknown recipient "${to}" \u2014 no agent spec, fleet member, guest or inbox owns that address`
160344
+ });
160345
+ }
160346
+ try {
160347
+ const result = await ctx.inbox().send({
160348
+ from: input.from ?? OPERATOR_INBOX_ID,
160349
+ to,
160350
+ body: input.body,
160351
+ ...input.kind !== undefined ? { kind: input.kind } : {},
160352
+ ...input.requestKey !== undefined ? { requestKey: input.requestKey } : {},
160353
+ ...input.taskId !== undefined ? { taskId: input.taskId } : {},
160354
+ ...input.parentRunId !== undefined ? { parentRunId: input.parentRunId } : {}
160355
+ });
160356
+ return { message: toMessageDto(result.message), replayed: result.replayed };
160357
+ } catch (error) {
160358
+ if (error instanceof ORPCError)
160359
+ throw error;
160360
+ throw toOrpcError(error, "BAD_REQUEST");
160361
+ }
160362
+ }),
160363
+ reply: os5.inbox.reply.handler(async ({ input }) => {
160364
+ try {
160365
+ return toMessageDto(await ctx.inbox().reply(input.id, input.from, input.body));
160366
+ } catch (error) {
160367
+ throw toOrpcError(error, "NOT_FOUND");
160368
+ }
160369
+ }),
160370
+ read: os5.inbox.read.handler(async ({ input }) => {
160371
+ try {
160372
+ const receipt = await ctx.inbox().markRead(input.id, input.reader);
160373
+ return { readAt: receipt.readAt, transitioned: receipt.transitioned };
160374
+ } catch (error) {
160375
+ if (!(error instanceof MessageNotFoundError))
160376
+ throw toOrpcError(error, "NOT_FOUND");
160377
+ throw new ORPCError("NOT_FOUND", { message: "message not found for this reader" });
160378
+ }
160379
+ }),
160380
+ requests: os5.inbox.requests.handler(async ({ input }) => {
160381
+ const orchestratorId = await orchestratorAddress(ctx);
160382
+ if (orchestratorId === "")
160383
+ return [];
160384
+ const limit = input.limit ?? 50;
160385
+ const rows = await ctx.inbox().requests(orchestratorId);
160386
+ const reconciler = new DeliveryReconciler({ getDb: () => ctx.getDb() });
160387
+ const classified = await reconciler.classify(orchestratorId);
160388
+ const reasonByMessage = new Map(classified.map((item) => [item.messageId, item.reason]));
160389
+ const holdByTask = new Map;
160390
+ try {
160391
+ const runtime = new StrategyRuntime({
160392
+ openDb: () => ctx.getDb(),
160393
+ tasks: ctx.taskService(),
160394
+ fleet: fleetOver(ctx),
160395
+ dependencyBlocked: async () => null
160396
+ });
160397
+ const selected = await runtime.selectNext(normalizeProjectPath(ctx.cwd), { readOnly: true });
160398
+ for (const hold of selected.holds)
160399
+ holdByTask.set(hold.wbs, hold.reason);
160400
+ } catch {}
160401
+ return rows.slice(0, limit).map((row) => ({
160402
+ messageId: row.id,
160403
+ requestKey: row.requestKey,
160404
+ status: row.status,
160405
+ attempts: row.attempts,
160406
+ error: row.error,
160407
+ runId: row.runId ?? null,
160408
+ taskId: row.runTaskId ?? row.taskId,
160409
+ outcome: row.runOutcome ?? null,
160410
+ reason: reasonByMessage.get(row.id) ?? null,
160411
+ hold: holdByTask.get(row.runTaskId ?? row.taskId ?? "") ?? null
160412
+ }));
160413
+ }),
160414
+ members: os5.inbox.members.handler(async () => ctx.inbox().members())
160415
+ };
160416
+ }
160417
+
160418
+ // ../server/src/router.ts
160419
+ var os6 = implement(contract);
159894
160420
  var EMPTY_BOARD_CATALOG = { catalogVersion: 1, host: null, modules: [] };
159895
160421
  var stubCtx = new Proxy({}, {
159896
160422
  get(_t, p) {
@@ -159899,7 +160425,7 @@ var stubCtx = new Proxy({}, {
159899
160425
  });
159900
160426
  function createRouter2(ctx) {
159901
160427
  return {
159902
- health: os5.health.handler(() => ({
160428
+ health: os6.health.handler(() => ({
159903
160429
  status: "ok",
159904
160430
  timestamp: new Date().toISOString(),
159905
160431
  service: "spur",
@@ -159908,10 +160434,11 @@ function createRouter2(ctx) {
159908
160434
  task: createTaskHandlers(ctx ?? stubCtx),
159909
160435
  feature: createFeatureHandlers(ctx ?? stubCtx),
159910
160436
  history: createHistoryHandlers(ctx ?? stubCtx),
160437
+ inbox: createInboxHandlers(ctx ?? stubCtx),
159911
160438
  board: {
159912
- modules: os5.board.modules.handler(() => ctx?.boardModules?.catalog ?? EMPTY_BOARD_CATALOG)
160439
+ modules: os6.board.modules.handler(() => ctx?.boardModules?.catalog ?? EMPTY_BOARD_CATALOG)
159913
160440
  },
159914
- stream: os5.stream.handler(async () => {
160441
+ stream: os6.stream.handler(async () => {
159915
160442
  throw new Error("SSE stream served by raw Hono route (modules/events)");
159916
160443
  })
159917
160444
  };
@@ -160100,6 +160627,7 @@ function createServerContext(appRt, options) {
160100
160627
  let featureSvc;
160101
160628
  let historyBoardSvc;
160102
160629
  let coordinationSvc;
160630
+ let inboxSvc;
160103
160631
  let supervisorSvc;
160104
160632
  let processInventorySvc;
160105
160633
  let processRegistrySvc;
@@ -160276,6 +160804,11 @@ function createServerContext(appRt, options) {
160276
160804
  }
160277
160805
  return coordinationSvc;
160278
160806
  },
160807
+ inbox() {
160808
+ if (!inboxSvc)
160809
+ inboxSvc = new InboxService({ getDb: this.getDb.bind(this) });
160810
+ return inboxSvc;
160811
+ },
160279
160812
  reloadAgentConfig: async () => {
160280
160813
  try {
160281
160814
  return await loadSpurConfig(cwd);
@@ -161784,6 +162317,7 @@ function registerWorkflowCleanCommand(workflow, context) {
161784
162317
  }
161785
162318
 
161786
162319
  // ../../packages/app/src/workflow/commit-guard.ts
162320
+ init_src();
161787
162321
  init_dist5();
161788
162322
  import { mkdirSync as mkdirSync23, readFileSync as readFileSync28, renameSync as renameSync10, writeFileSync as writeFileSync20 } from "fs";
161789
162323
  import { join as join100 } from "path";
@@ -161954,6 +162488,26 @@ function commitGuardStage(run, paths, options = {}) {
161954
162488
  foreign
161955
162489
  };
161956
162490
  }
162491
+ function foreignSessionOwner(cwd) {
162492
+ try {
162493
+ const raw = JSON.parse(readFileSync28(join100(cwd, ".spur", "context", ".session.json"), "utf8"));
162494
+ const sessionId = typeof raw.owner?.sessionId === "string" ? raw.owner.sessionId : "";
162495
+ if (sessionId === "")
162496
+ return null;
162497
+ const current = currentHostSessionId();
162498
+ if (current === "" || current === sessionId)
162499
+ return null;
162500
+ return { host: typeof raw.owner?.host === "string" ? raw.owner.host : "unknown", sessionId };
162501
+ } catch {
162502
+ return null;
162503
+ }
162504
+ }
162505
+ function currentHostSessionId() {
162506
+ const transcript = getEnvVar("PI_SESSION_FILE") ?? "";
162507
+ const stem = transcript.replace(/\.jsonl$/, "").split("/").pop() ?? "";
162508
+ const pi = stem.includes("_") ? stem.split("_").pop() ?? "" : "";
162509
+ return pi !== "" ? pi : (getEnvVar("CLAUDE_CODE_SESSION_ID") ?? "").trim();
162510
+ }
161957
162511
  function commitGuardCheck(run, options = {}) {
161958
162512
  try {
161959
162513
  const context = runContext(run, options);
@@ -161961,7 +162515,11 @@ function commitGuardCheck(run, options = {}) {
161961
162515
  return missingStart(run);
161962
162516
  const { base, git, written } = context;
161963
162517
  const foreign = [...changedSinceBase(base, git)].filter((path) => !written.has(path)).sort();
161964
- return report(0, [JSON.stringify({ foreign })]);
162518
+ const owner = foreignSessionOwner(options.cwd ?? process.cwd());
162519
+ const stderr = owner ? [
162520
+ `commit-guard: another session owns this tree \u2014 .spur/context/.session.json records ${owner.host} ${owner.sessionId}; use a separate worktree (one writer per tree)`
162521
+ ] : [];
162522
+ return { ...report(0, [JSON.stringify({ foreign, ...owner ? { foreignSession: owner } : {} })], stderr) };
161965
162523
  } catch (err) {
161966
162524
  return report(1, [], [probeDiagnostic(err)]);
161967
162525
  }