@useorgx/openclaw-plugin 0.7.20 → 0.7.24

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 (165) hide show
  1. package/dashboard/dist/assets/B6VftyY6.js +1 -0
  2. package/dashboard/dist/assets/B6VftyY6.js.br +0 -0
  3. package/dashboard/dist/assets/B6VftyY6.js.gz +0 -0
  4. package/dashboard/dist/assets/{Dm0CfDGr.js → BANQdlC4.js} +1 -1
  5. package/dashboard/dist/assets/BANQdlC4.js.br +0 -0
  6. package/dashboard/dist/assets/BANQdlC4.js.gz +0 -0
  7. package/dashboard/dist/assets/{_zpQCpjm.js → BPL4CL3c.js} +1 -1
  8. package/dashboard/dist/assets/BPL4CL3c.js.br +0 -0
  9. package/dashboard/dist/assets/BPL4CL3c.js.gz +0 -0
  10. package/dashboard/dist/assets/{DXVs61e1.js → BZCkOZ20.js} +1 -1
  11. package/dashboard/dist/assets/BZCkOZ20.js.br +0 -0
  12. package/dashboard/dist/assets/BZCkOZ20.js.gz +0 -0
  13. package/dashboard/dist/assets/{BYb6DARX.js → B_LdOJUa.js} +1 -1
  14. package/dashboard/dist/assets/B_LdOJUa.js.br +0 -0
  15. package/dashboard/dist/assets/B_LdOJUa.js.gz +0 -0
  16. package/dashboard/dist/assets/Bfp-wdwb.css +1 -0
  17. package/dashboard/dist/assets/Bfp-wdwb.css.br +0 -0
  18. package/dashboard/dist/assets/Bfp-wdwb.css.gz +0 -0
  19. package/dashboard/dist/assets/{DibzNd0I.js → BvFcH_Iy.js} +1 -1
  20. package/dashboard/dist/assets/BvFcH_Iy.js.br +0 -0
  21. package/dashboard/dist/assets/BvFcH_Iy.js.gz +0 -0
  22. package/dashboard/dist/assets/By0MIBj_.js +1 -0
  23. package/dashboard/dist/assets/By0MIBj_.js.br +0 -0
  24. package/dashboard/dist/assets/By0MIBj_.js.gz +0 -0
  25. package/dashboard/dist/assets/C0i7ABUU.js +212 -0
  26. package/dashboard/dist/assets/C0i7ABUU.js.br +0 -0
  27. package/dashboard/dist/assets/C0i7ABUU.js.gz +0 -0
  28. package/dashboard/dist/assets/CFB0MM7j.js +1 -0
  29. package/dashboard/dist/assets/CFB0MM7j.js.br +0 -0
  30. package/dashboard/dist/assets/CFB0MM7j.js.gz +0 -0
  31. package/dashboard/dist/assets/CQSRb1yu.js +1 -0
  32. package/dashboard/dist/assets/CQSRb1yu.js.br +0 -0
  33. package/dashboard/dist/assets/CQSRb1yu.js.gz +0 -0
  34. package/dashboard/dist/assets/{wa4jJQK9.js → CUoQoSm-.js} +1 -1
  35. package/dashboard/dist/assets/CUoQoSm-.js.br +0 -0
  36. package/dashboard/dist/assets/CUoQoSm-.js.gz +0 -0
  37. package/dashboard/dist/assets/Ckd1R1iE.js +1 -0
  38. package/dashboard/dist/assets/Ckd1R1iE.js.br +0 -0
  39. package/dashboard/dist/assets/Ckd1R1iE.js.gz +0 -0
  40. package/dashboard/dist/assets/{BGY6oI8h.js → CqRNb2EL.js} +1 -1
  41. package/dashboard/dist/assets/CqRNb2EL.js.br +0 -0
  42. package/dashboard/dist/assets/CqRNb2EL.js.gz +0 -0
  43. package/dashboard/dist/assets/{DAr4MfFk.js → DClUc9rw.js} +1 -1
  44. package/dashboard/dist/assets/DClUc9rw.js.br +0 -0
  45. package/dashboard/dist/assets/DClUc9rw.js.gz +0 -0
  46. package/dashboard/dist/assets/DF2PMTwT.js +1 -0
  47. package/dashboard/dist/assets/DF2PMTwT.js.br +0 -0
  48. package/dashboard/dist/assets/DF2PMTwT.js.gz +0 -0
  49. package/dashboard/dist/assets/{B014hrCe.js → DJYl7gyA.js} +2 -2
  50. package/dashboard/dist/assets/DJYl7gyA.js.br +0 -0
  51. package/dashboard/dist/assets/DJYl7gyA.js.gz +0 -0
  52. package/dashboard/dist/assets/{BoDhb8_y.js → DZtNMX0t.js} +2 -2
  53. package/dashboard/dist/assets/DZtNMX0t.js.br +0 -0
  54. package/dashboard/dist/assets/DZtNMX0t.js.gz +0 -0
  55. package/dashboard/dist/assets/DlEa8PI0.js +1 -0
  56. package/dashboard/dist/assets/DlEa8PI0.js.br +0 -0
  57. package/dashboard/dist/assets/DlEa8PI0.js.gz +0 -0
  58. package/dashboard/dist/assets/M4QxcXjh.js +1 -0
  59. package/dashboard/dist/assets/M4QxcXjh.js.br +0 -0
  60. package/dashboard/dist/assets/M4QxcXjh.js.gz +0 -0
  61. package/dashboard/dist/assets/{CV0sWMbv.js → MrW1ixGx.js} +1 -1
  62. package/dashboard/dist/assets/MrW1ixGx.js.br +0 -0
  63. package/dashboard/dist/assets/MrW1ixGx.js.gz +0 -0
  64. package/dashboard/dist/index.html +2 -2
  65. package/dashboard/dist/index.html.br +0 -0
  66. package/dashboard/dist/index.html.gz +0 -0
  67. package/dist/activity-store.js +68 -8
  68. package/dist/agent-run-store.js +162 -24
  69. package/dist/cli/orgx.d.ts +3 -0
  70. package/dist/config/resolution.d.ts +7 -0
  71. package/dist/config/resolution.js +13 -5
  72. package/dist/contracts/onboarding-state.d.ts +2 -0
  73. package/dist/contracts/onboarding-state.js +23 -0
  74. package/dist/contracts/shared-types.d.ts +45 -0
  75. package/dist/http/helpers/auto-continue-engine.d.ts +23 -0
  76. package/dist/http/helpers/auto-continue-engine.js +468 -85
  77. package/dist/http/helpers/autopilot-runtime.js +5 -1
  78. package/dist/http/helpers/autopilot-slice-utils.js +25 -1
  79. package/dist/http/helpers/decision-mapper.d.ts +1 -0
  80. package/dist/http/helpers/decision-mapper.js +19 -2
  81. package/dist/http/helpers/dispatch-lifecycle.js +3 -0
  82. package/dist/http/helpers/mission-control.d.ts +1 -0
  83. package/dist/http/helpers/mission-control.js +5 -2
  84. package/dist/http/helpers/slice-run-projections.d.ts +27 -0
  85. package/dist/http/helpers/slice-run-projections.js +198 -10
  86. package/dist/http/helpers/triage-mapper.js +499 -6
  87. package/dist/http/helpers/value-utils.d.ts +1 -0
  88. package/dist/http/helpers/value-utils.js +17 -0
  89. package/dist/http/index.d.ts +1 -0
  90. package/dist/http/index.js +179 -46
  91. package/dist/http/router.js +64 -9
  92. package/dist/http/routes/live-legacy.d.ts +19 -2
  93. package/dist/http/routes/live-legacy.js +110 -27
  94. package/dist/http/routes/live-snapshot.d.ts +16 -2
  95. package/dist/http/routes/live-snapshot.js +169 -25
  96. package/dist/http/routes/live-triage.js +6 -1
  97. package/dist/http/routes/mission-control-actions.d.ts +9 -0
  98. package/dist/http/routes/mission-control-actions.js +185 -7
  99. package/dist/http/routes/mission-control-read.d.ts +13 -0
  100. package/dist/http/routes/mission-control-read.js +100 -219
  101. package/dist/http/routes/onboarding.d.ts +1 -0
  102. package/dist/http/routes/onboarding.js +17 -0
  103. package/dist/index.d.ts +5 -0
  104. package/dist/index.js +199 -123
  105. package/dist/outbox.d.ts +0 -2
  106. package/dist/outbox.js +259 -148
  107. package/dist/reporting/rollups.js +18 -11
  108. package/dist/runtime-instance-store.js +212 -58
  109. package/dist/stores/materialized-snapshot-store.d.ts +18 -0
  110. package/dist/stores/materialized-snapshot-store.js +91 -0
  111. package/dist/stores/sqlite-state.d.ts +6 -0
  112. package/dist/stores/sqlite-state.js +330 -0
  113. package/package.json +5 -1
  114. package/dashboard/dist/assets/B014hrCe.js.br +0 -0
  115. package/dashboard/dist/assets/B014hrCe.js.gz +0 -0
  116. package/dashboard/dist/assets/BCudUvwg.js +0 -1
  117. package/dashboard/dist/assets/BCudUvwg.js.br +0 -0
  118. package/dashboard/dist/assets/BCudUvwg.js.gz +0 -0
  119. package/dashboard/dist/assets/BGY6oI8h.js.br +0 -0
  120. package/dashboard/dist/assets/BGY6oI8h.js.gz +0 -0
  121. package/dashboard/dist/assets/BJI1Iy5v.css +0 -1
  122. package/dashboard/dist/assets/BJI1Iy5v.css.br +0 -0
  123. package/dashboard/dist/assets/BJI1Iy5v.css.gz +0 -0
  124. package/dashboard/dist/assets/BUvcp_7V.js +0 -1
  125. package/dashboard/dist/assets/BUvcp_7V.js.br +0 -0
  126. package/dashboard/dist/assets/BUvcp_7V.js.gz +0 -0
  127. package/dashboard/dist/assets/BV2Tf8S2.js +0 -212
  128. package/dashboard/dist/assets/BV2Tf8S2.js.br +0 -0
  129. package/dashboard/dist/assets/BV2Tf8S2.js.gz +0 -0
  130. package/dashboard/dist/assets/BYb6DARX.js.br +0 -0
  131. package/dashboard/dist/assets/BYb6DARX.js.gz +0 -0
  132. package/dashboard/dist/assets/BoDhb8_y.js.br +0 -0
  133. package/dashboard/dist/assets/BoDhb8_y.js.gz +0 -0
  134. package/dashboard/dist/assets/Bqk_l0k6.js +0 -1
  135. package/dashboard/dist/assets/Bqk_l0k6.js.br +0 -0
  136. package/dashboard/dist/assets/Bqk_l0k6.js.gz +0 -0
  137. package/dashboard/dist/assets/C-MOJWHs.js +0 -1
  138. package/dashboard/dist/assets/C-MOJWHs.js.br +0 -0
  139. package/dashboard/dist/assets/C-MOJWHs.js.gz +0 -0
  140. package/dashboard/dist/assets/CV0sWMbv.js.br +0 -0
  141. package/dashboard/dist/assets/CV0sWMbv.js.gz +0 -0
  142. package/dashboard/dist/assets/CaAkScfa.js +0 -1
  143. package/dashboard/dist/assets/CaAkScfa.js.br +0 -0
  144. package/dashboard/dist/assets/CaAkScfa.js.gz +0 -0
  145. package/dashboard/dist/assets/Ck5KlsPN.js +0 -1
  146. package/dashboard/dist/assets/Ck5KlsPN.js.br +0 -0
  147. package/dashboard/dist/assets/Ck5KlsPN.js.gz +0 -0
  148. package/dashboard/dist/assets/D2G51wQm.js +0 -1
  149. package/dashboard/dist/assets/D2G51wQm.js.br +0 -0
  150. package/dashboard/dist/assets/D2G51wQm.js.gz +0 -0
  151. package/dashboard/dist/assets/DAr4MfFk.js.br +0 -0
  152. package/dashboard/dist/assets/DAr4MfFk.js.gz +0 -0
  153. package/dashboard/dist/assets/DXVs61e1.js.br +0 -0
  154. package/dashboard/dist/assets/DXVs61e1.js.gz +0 -0
  155. package/dashboard/dist/assets/DibzNd0I.js.br +0 -0
  156. package/dashboard/dist/assets/DibzNd0I.js.gz +0 -0
  157. package/dashboard/dist/assets/Dm0CfDGr.js.br +0 -0
  158. package/dashboard/dist/assets/Dm0CfDGr.js.gz +0 -0
  159. package/dashboard/dist/assets/_zpQCpjm.js.br +0 -0
  160. package/dashboard/dist/assets/_zpQCpjm.js.gz +0 -0
  161. package/dashboard/dist/assets/uNGpYMSH.js +0 -1
  162. package/dashboard/dist/assets/uNGpYMSH.js.br +0 -0
  163. package/dashboard/dist/assets/uNGpYMSH.js.gz +0 -0
  164. package/dashboard/dist/assets/wa4jJQK9.js.br +0 -0
  165. package/dashboard/dist/assets/wa4jJQK9.js.gz +0 -0
package/dist/outbox.js CHANGED
@@ -4,10 +4,11 @@
4
4
  * Events are flushed on next successful sync.
5
5
  */
6
6
  import { join } from "node:path";
7
- import { readFile, writeFile, mkdir, chmod, rename, unlink, readdir, appendFile, } from "node:fs/promises";
7
+ import { readFile, mkdir, chmod, rename, unlink, readdir, appendFile, } from "node:fs/promises";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { classifyOutboxReplaySkip } from "./event-sanitization.js";
10
10
  import { getOrgxOutboxDir } from "./paths.js";
11
+ import { getStateDb, readStateMeta, writeStateMeta, } from "./stores/sqlite-state.js";
11
12
  function outboxDir() {
12
13
  return getOrgxOutboxDir();
13
14
  }
@@ -42,6 +43,7 @@ const OUTBOX_EVENT_TTL_MS = 7 * 24 * 60 * 60_000;
42
43
  /** Hard cap per session to prevent unbounded growth if sync repeatedly fails. */
43
44
  const OUTBOX_MAX_EVENTS_PER_SESSION = 500;
44
45
  const DEAD_LETTER_DIRNAME = "_dead-letter";
46
+ const OUTBOX_IMPORT_META_KEY = "outbox_events_imported_v1";
45
47
  async function ensureDir() {
46
48
  const dir = outboxDir();
47
49
  try {
@@ -62,7 +64,6 @@ function outboxPath(sessionId) {
62
64
  return join(outboxDir(), `${normalizeSessionId(sessionId)}.json`);
63
65
  }
64
66
  async function backupCorruptOutboxFile(targetPath) {
65
- // Preserve the corrupted file for debugging, but move it out of the hot path.
66
67
  const suffix = `${Date.now()}-${randomUUID().slice(0, 8)}`;
67
68
  const backupPath = `${targetPath}.corrupt.${suffix}`;
68
69
  try {
@@ -73,34 +74,6 @@ async function backupCorruptOutboxFile(targetPath) {
73
74
  // best effort
74
75
  }
75
76
  }
76
- async function writeFileAtomic(targetPath, content, mode) {
77
- // Atomic write to avoid partial JSON files if we crash mid-write.
78
- const tmpPath = `${targetPath}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
79
- await writeFile(tmpPath, content, { encoding: "utf8", mode });
80
- await hardenPath(tmpPath, mode);
81
- try {
82
- await rename(tmpPath, targetPath);
83
- }
84
- catch (err) {
85
- // On Windows, rename can fail if the destination exists. Best-effort fallback:
86
- // remove the destination and retry. This is not strictly atomic, but avoids
87
- // leaving the outbox unreadable.
88
- const code = err && typeof err === "object" ? err.code : null;
89
- if (code === "EEXIST" || code === "EPERM" || code === "EACCES") {
90
- try {
91
- await unlink(targetPath);
92
- }
93
- catch {
94
- // ignore
95
- }
96
- await rename(tmpPath, targetPath);
97
- }
98
- else {
99
- throw err;
100
- }
101
- }
102
- await hardenPath(targetPath, mode);
103
- }
104
77
  async function appendOutboxDeadLetterRecord(sessionId, record) {
105
78
  await ensureDir();
106
79
  const dir = deadLetterDir();
@@ -130,6 +103,27 @@ export async function appendOutboxDeadLetter(sessionId, event, reason, error) {
130
103
  event,
131
104
  });
132
105
  }
106
+ function normalizeOutboxEvent(input) {
107
+ const timestamp = typeof input.timestamp === "string" && Number.isFinite(Date.parse(input.timestamp))
108
+ ? new Date(Date.parse(input.timestamp)).toISOString()
109
+ : new Date().toISOString();
110
+ return {
111
+ id: input.id.trim(),
112
+ type: input.type,
113
+ timestamp,
114
+ payload: input.payload && typeof input.payload === "object" && !Array.isArray(input.payload)
115
+ ? input.payload
116
+ : {},
117
+ activityItem: input.activityItem,
118
+ replayFailures: typeof input.replayFailures === "number" && Number.isFinite(input.replayFailures)
119
+ ? Math.max(0, Math.floor(input.replayFailures))
120
+ : undefined,
121
+ lastReplayError: typeof input.lastReplayError === "string" ? input.lastReplayError : input.lastReplayError ?? null,
122
+ lastReplayAt: typeof input.lastReplayAt === "string" && Number.isFinite(Date.parse(input.lastReplayAt))
123
+ ? new Date(Date.parse(input.lastReplayAt)).toISOString()
124
+ : input.lastReplayAt ?? null,
125
+ };
126
+ }
133
127
  /** Drop stale events and enforce per-session cap. Returns true if any were removed. */
134
128
  function pruneOutboxEvents(events) {
135
129
  const cutoff = Date.now() - OUTBOX_EVENT_TTL_MS;
@@ -137,50 +131,67 @@ function pruneOutboxEvents(events) {
137
131
  const epoch = Date.parse(e.timestamp);
138
132
  return Number.isFinite(epoch) && epoch >= cutoff;
139
133
  });
140
- // Keep newest events if over cap.
141
134
  const capped = fresh.length > OUTBOX_MAX_EVENTS_PER_SESSION
142
135
  ? fresh.slice(fresh.length - OUTBOX_MAX_EVENTS_PER_SESSION)
143
136
  : fresh;
144
137
  return { pruned: capped, changed: capped.length !== events.length };
145
138
  }
146
- export async function readOutbox(sessionId) {
147
- let targetPath;
148
- try {
149
- targetPath = outboxPath(sessionId);
150
- }
151
- catch {
152
- return [];
153
- }
139
+ function rowToEvent(row) {
140
+ const payload = row.payload_json && typeof row.payload_json === "string"
141
+ ? JSON.parse(row.payload_json)
142
+ : {};
143
+ const activityItem = row.activity_item_json && typeof row.activity_item_json === "string"
144
+ ? JSON.parse(row.activity_item_json)
145
+ : null;
146
+ if (!activityItem || typeof activityItem !== "object")
147
+ return null;
148
+ return normalizeOutboxEvent({
149
+ id: row.event_id,
150
+ type: row.event_type,
151
+ timestamp: row.timestamp,
152
+ payload: payload && typeof payload === "object" && !Array.isArray(payload)
153
+ ? payload
154
+ : {},
155
+ activityItem: activityItem,
156
+ replayFailures: typeof row.replay_failures === "number" ? row.replay_failures : undefined,
157
+ lastReplayError: row.last_replay_error,
158
+ lastReplayAt: row.last_replay_at,
159
+ });
160
+ }
161
+ function writeOutboxEvents(queueId, events) {
162
+ const normalizedQueueId = normalizeSessionId(queueId);
163
+ const transaction = getStateDb().transaction((items) => {
164
+ getStateDb()
165
+ .prepare("DELETE FROM outbox_events WHERE queue_id = ?")
166
+ .run(normalizedQueueId);
167
+ const insertStatement = getStateDb().prepare(`INSERT INTO outbox_events (
168
+ event_id,
169
+ queue_id,
170
+ event_type,
171
+ timestamp,
172
+ payload_json,
173
+ activity_item_json,
174
+ replay_failures,
175
+ last_replay_error,
176
+ last_replay_at,
177
+ updated_at
178
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
179
+ const nowIso = new Date().toISOString();
180
+ for (const event of items) {
181
+ const normalized = normalizeOutboxEvent(event);
182
+ insertStatement.run(normalized.id, normalizedQueueId, normalized.type, normalized.timestamp, JSON.stringify(normalized.payload ?? {}), JSON.stringify(normalized.activityItem), normalized.replayFailures ?? 0, normalized.lastReplayError ?? null, normalized.lastReplayAt ?? null, nowIso);
183
+ }
184
+ });
185
+ transaction(events);
186
+ }
187
+ async function readLegacyOutboxFile(queueId) {
188
+ const targetPath = outboxPath(queueId);
154
189
  try {
155
190
  const raw = await readFile(targetPath, "utf8");
156
191
  try {
157
192
  const parsed = JSON.parse(raw);
158
193
  const events = Array.isArray(parsed) ? parsed : [];
159
- const { pruned, changed } = pruneOutboxEvents(events);
160
- const filtered = [];
161
- let filteredChanged = changed;
162
- for (const event of pruned) {
163
- const skipReason = classifyOutboxReplaySkip(event);
164
- if (!skipReason) {
165
- filtered.push(event);
166
- continue;
167
- }
168
- filteredChanged = true;
169
- await appendOutboxDeadLetter(sessionId, event, `pruned_on_read:${skipReason}`, null);
170
- }
171
- // Write back if stale events were dropped.
172
- if (filteredChanged) {
173
- if (filtered.length === 0) {
174
- try {
175
- await unlink(targetPath);
176
- }
177
- catch { /* ok */ }
178
- }
179
- else {
180
- await writeFileAtomic(targetPath, JSON.stringify(filtered), 0o600);
181
- }
182
- }
183
- return filtered;
194
+ return events.map((event) => normalizeOutboxEvent(event));
184
195
  }
185
196
  catch {
186
197
  await backupCorruptOutboxFile(targetPath);
@@ -191,6 +202,103 @@ export async function readOutbox(sessionId) {
191
202
  return [];
192
203
  }
193
204
  }
205
+ async function ensureOutboxMigrated() {
206
+ const migrated = readStateMeta(OUTBOX_IMPORT_META_KEY);
207
+ if (migrated)
208
+ return;
209
+ const countRow = getStateDb()
210
+ .prepare("SELECT COUNT(*) AS count FROM outbox_events")
211
+ .get();
212
+ if ((countRow?.count ?? 0) > 0) {
213
+ writeStateMeta(OUTBOX_IMPORT_META_KEY, true);
214
+ return;
215
+ }
216
+ await ensureDir();
217
+ const files = await readdir(outboxDir()).catch(() => []);
218
+ const queueIds = files
219
+ .filter((file) => file.endsWith(".json"))
220
+ .map((file) => file.slice(0, -5))
221
+ .filter(Boolean);
222
+ for (const queueId of queueIds) {
223
+ const events = await readLegacyOutboxFile(queueId);
224
+ if (events.length === 0)
225
+ continue;
226
+ writeOutboxEvents(queueId, events);
227
+ }
228
+ writeStateMeta(OUTBOX_IMPORT_META_KEY, true);
229
+ }
230
+ function pruneQueueRows(queueId) {
231
+ const normalizedQueueId = normalizeSessionId(queueId);
232
+ const cutoffIso = new Date(Date.now() - OUTBOX_EVENT_TTL_MS).toISOString();
233
+ getStateDb()
234
+ .prepare(`DELETE FROM outbox_events
235
+ WHERE queue_id = ?
236
+ AND timestamp < ?`)
237
+ .run(normalizedQueueId, cutoffIso);
238
+ getStateDb()
239
+ .prepare(`DELETE FROM outbox_events
240
+ WHERE queue_id = ?
241
+ AND event_id NOT IN (
242
+ SELECT event_id
243
+ FROM outbox_events
244
+ WHERE queue_id = ?
245
+ ORDER BY timestamp DESC, updated_at DESC
246
+ LIMIT ?
247
+ )`)
248
+ .run(normalizedQueueId, normalizedQueueId, OUTBOX_MAX_EVENTS_PER_SESSION);
249
+ }
250
+ export async function readOutbox(sessionId) {
251
+ let normalizedSessionId;
252
+ try {
253
+ normalizedSessionId = normalizeSessionId(sessionId);
254
+ }
255
+ catch {
256
+ return [];
257
+ }
258
+ await ensureOutboxMigrated();
259
+ const rows = getStateDb()
260
+ .prepare(`SELECT
261
+ queue_id,
262
+ event_id,
263
+ event_type,
264
+ timestamp,
265
+ payload_json,
266
+ activity_item_json,
267
+ replay_failures,
268
+ last_replay_error,
269
+ last_replay_at
270
+ FROM outbox_events
271
+ WHERE queue_id = ?
272
+ ORDER BY timestamp ASC, event_id ASC`)
273
+ .all(normalizedSessionId);
274
+ const events = [];
275
+ for (const row of rows) {
276
+ try {
277
+ const event = rowToEvent(row);
278
+ if (event)
279
+ events.push(event);
280
+ }
281
+ catch {
282
+ // skip malformed rows
283
+ }
284
+ }
285
+ const { pruned, changed } = pruneOutboxEvents(events);
286
+ const filtered = [];
287
+ let filteredChanged = changed;
288
+ for (const event of pruned) {
289
+ const skipReason = classifyOutboxReplaySkip(event);
290
+ if (!skipReason) {
291
+ filtered.push(event);
292
+ continue;
293
+ }
294
+ filteredChanged = true;
295
+ await appendOutboxDeadLetter(normalizedSessionId, event, `pruned_on_read:${skipReason}`, null);
296
+ }
297
+ if (filteredChanged) {
298
+ await replaceOutbox(normalizedSessionId, filtered);
299
+ }
300
+ return filtered;
301
+ }
194
302
  export async function appendToOutbox(sessionId, event) {
195
303
  let normalizedSessionId;
196
304
  try {
@@ -199,22 +307,39 @@ export async function appendToOutbox(sessionId, event) {
199
307
  catch {
200
308
  return;
201
309
  }
202
- const skipReason = classifyOutboxReplaySkip(event);
310
+ const normalizedEvent = normalizeOutboxEvent(event);
311
+ const skipReason = classifyOutboxReplaySkip(normalizedEvent);
203
312
  if (skipReason) {
204
- await appendOutboxDeadLetter(normalizedSessionId, event, `suppressed_on_append:${skipReason}`, null);
313
+ await appendOutboxDeadLetter(normalizedSessionId, normalizedEvent, `suppressed_on_append:${skipReason}`, null);
205
314
  return;
206
315
  }
207
- await ensureDir();
208
- const targetPath = outboxPath(normalizedSessionId);
209
- const existing = await readOutbox(normalizedSessionId);
210
- const idx = existing.findIndex((item) => item.id === event.id);
211
- if (idx >= 0) {
212
- existing[idx] = event;
213
- }
214
- else {
215
- existing.push(event);
216
- }
217
- await writeFileAtomic(targetPath, JSON.stringify(existing), 0o600);
316
+ await ensureOutboxMigrated();
317
+ const nowIso = new Date().toISOString();
318
+ getStateDb()
319
+ .prepare(`INSERT INTO outbox_events (
320
+ event_id,
321
+ queue_id,
322
+ event_type,
323
+ timestamp,
324
+ payload_json,
325
+ activity_item_json,
326
+ replay_failures,
327
+ last_replay_error,
328
+ last_replay_at,
329
+ updated_at
330
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
331
+ ON CONFLICT(event_id) DO UPDATE SET
332
+ queue_id = excluded.queue_id,
333
+ event_type = excluded.event_type,
334
+ timestamp = excluded.timestamp,
335
+ payload_json = excluded.payload_json,
336
+ activity_item_json = excluded.activity_item_json,
337
+ replay_failures = excluded.replay_failures,
338
+ last_replay_error = excluded.last_replay_error,
339
+ last_replay_at = excluded.last_replay_at,
340
+ updated_at = excluded.updated_at`)
341
+ .run(normalizedEvent.id, normalizedSessionId, normalizedEvent.type, normalizedEvent.timestamp, JSON.stringify(normalizedEvent.payload ?? {}), JSON.stringify(normalizedEvent.activityItem), normalizedEvent.replayFailures ?? 0, normalizedEvent.lastReplayError ?? null, normalizedEvent.lastReplayAt ?? null, nowIso);
342
+ pruneQueueRows(normalizedSessionId);
218
343
  }
219
344
  export async function replaceOutbox(sessionId, events) {
220
345
  let normalizedSessionId;
@@ -224,93 +349,75 @@ export async function replaceOutbox(sessionId, events) {
224
349
  catch {
225
350
  return;
226
351
  }
227
- await ensureDir();
228
- const targetPath = outboxPath(normalizedSessionId);
352
+ await ensureOutboxMigrated();
229
353
  if (events.length === 0) {
354
+ getStateDb()
355
+ .prepare("DELETE FROM outbox_events WHERE queue_id = ?")
356
+ .run(normalizedSessionId);
230
357
  try {
231
- await unlink(targetPath);
232
- return;
358
+ await unlink(outboxPath(normalizedSessionId));
233
359
  }
234
360
  catch {
235
361
  // File may not exist
236
- return;
237
362
  }
363
+ return;
238
364
  }
239
- await writeFileAtomic(targetPath, JSON.stringify(events), 0o600);
365
+ writeOutboxEvents(normalizedSessionId, events);
366
+ pruneQueueRows(normalizedSessionId);
240
367
  }
241
368
  export async function readAllOutboxItems() {
242
- try {
243
- await ensureDir();
244
- const files = await readdir(outboxDir());
245
- const items = [];
246
- for (const file of files) {
247
- if (!file.endsWith(".json"))
248
- continue;
249
- const sessionId = file.slice(0, -5);
250
- try {
251
- const events = await readOutbox(sessionId);
252
- for (const evt of events) {
253
- items.push(evt.activityItem);
254
- }
255
- }
256
- catch {
257
- // skip malformed files
369
+ await ensureOutboxMigrated();
370
+ const rows = getStateDb()
371
+ .prepare(`SELECT activity_item_json
372
+ FROM outbox_events
373
+ ORDER BY timestamp DESC, event_id DESC
374
+ LIMIT ?`)
375
+ .all(10_000);
376
+ const items = [];
377
+ for (const row of rows) {
378
+ try {
379
+ const parsed = JSON.parse(row.activity_item_json);
380
+ if (parsed && typeof parsed === "object") {
381
+ items.push(parsed);
258
382
  }
259
383
  }
260
- return items.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
261
- }
262
- catch {
263
- return [];
384
+ catch {
385
+ // skip malformed rows
386
+ }
264
387
  }
388
+ return items;
265
389
  }
266
390
  export async function readOutboxSummary() {
267
- try {
268
- await ensureDir();
269
- const files = await readdir(outboxDir());
270
- const pendingByQueue = {};
271
- let pendingTotal = 0;
272
- let oldestEpoch = Number.POSITIVE_INFINITY;
273
- let newestEpoch = Number.NEGATIVE_INFINITY;
274
- for (const file of files) {
275
- if (!file.endsWith(".json"))
276
- continue;
277
- const queueId = file.slice(0, -5);
278
- try {
279
- const events = await readOutbox(queueId);
280
- const count = Array.isArray(events) ? events.length : 0;
281
- pendingByQueue[queueId] = count;
282
- pendingTotal += count;
283
- for (const event of events) {
284
- const epoch = Date.parse(event.timestamp);
285
- if (!Number.isFinite(epoch))
286
- continue;
287
- oldestEpoch = Math.min(oldestEpoch, epoch);
288
- newestEpoch = Math.max(newestEpoch, epoch);
289
- }
290
- }
291
- catch {
292
- pendingByQueue[queueId] = pendingByQueue[queueId] ?? 0;
293
- }
391
+ await ensureOutboxMigrated();
392
+ const rows = getStateDb()
393
+ .prepare(`SELECT
394
+ queue_id,
395
+ COUNT(*) AS pending_count,
396
+ MIN(timestamp) AS oldest_event_at,
397
+ MAX(timestamp) AS newest_event_at
398
+ FROM outbox_events
399
+ GROUP BY queue_id`)
400
+ .all();
401
+ const pendingByQueue = {};
402
+ let pendingTotal = 0;
403
+ let oldestEventAt = null;
404
+ let newestEventAt = null;
405
+ for (const row of rows) {
406
+ pendingByQueue[row.queue_id] = row.pending_count;
407
+ pendingTotal += row.pending_count;
408
+ if (row.oldest_event_at && (!oldestEventAt || row.oldest_event_at < oldestEventAt)) {
409
+ oldestEventAt = row.oldest_event_at;
410
+ }
411
+ if (row.newest_event_at && (!newestEventAt || row.newest_event_at > newestEventAt)) {
412
+ newestEventAt = row.newest_event_at;
294
413
  }
295
- return {
296
- pendingTotal,
297
- pendingByQueue,
298
- oldestEventAt: Number.isFinite(oldestEpoch)
299
- ? new Date(oldestEpoch).toISOString()
300
- : null,
301
- newestEventAt: Number.isFinite(newestEpoch)
302
- ? new Date(newestEpoch).toISOString()
303
- : null,
304
- };
305
- }
306
- catch {
307
- return {
308
- pendingTotal: 0,
309
- pendingByQueue: {},
310
- oldestEventAt: null,
311
- newestEventAt: null,
312
- };
313
414
  }
415
+ return {
416
+ pendingTotal,
417
+ pendingByQueue,
418
+ oldestEventAt,
419
+ newestEventAt,
420
+ };
314
421
  }
315
422
  export async function clearOutbox(sessionId) {
316
423
  let normalizedSessionId;
@@ -320,6 +427,10 @@ export async function clearOutbox(sessionId) {
320
427
  catch {
321
428
  return;
322
429
  }
430
+ await ensureOutboxMigrated();
431
+ getStateDb()
432
+ .prepare("DELETE FROM outbox_events WHERE queue_id = ?")
433
+ .run(normalizedSessionId);
323
434
  try {
324
435
  await unlink(outboxPath(normalizedSessionId));
325
436
  }
@@ -1,20 +1,27 @@
1
1
  export function classifyTaskState(status) {
2
2
  const normalized = String(status ?? "").trim().toLowerCase();
3
- if (normalized === "done" ||
4
- normalized === "completed" ||
5
- normalized === "cancelled" ||
6
- normalized === "archived" ||
7
- normalized === "deleted") {
3
+ const canonical = normalized.replace(/[\s-]+/g, "_");
4
+ if (canonical === "done" ||
5
+ canonical === "completed" ||
6
+ canonical === "cancelled" ||
7
+ canonical === "archived" ||
8
+ canonical === "deleted") {
8
9
  return "done";
9
10
  }
10
- if (normalized === "blocked" || normalized === "at_risk") {
11
+ if (canonical === "blocked" ||
12
+ canonical === "at_risk" ||
13
+ canonical === "on_hold" ||
14
+ canonical === "onhold") {
11
15
  return "blocked";
12
16
  }
13
- if (normalized === "in_progress" ||
14
- normalized === "active" ||
15
- normalized === "running" ||
16
- normalized === "queued" ||
17
- normalized === "retry_pending") {
17
+ if (canonical === "in_progress" ||
18
+ canonical === "inprogress" ||
19
+ canonical === "active" ||
20
+ canonical === "running" ||
21
+ canonical === "queued" ||
22
+ canonical === "retry_pending" ||
23
+ canonical === "pending_review" ||
24
+ canonical === "pendingreview") {
18
25
  return "active";
19
26
  }
20
27
  return "todo";