@useorgx/openclaw-plugin 0.7.18 → 0.7.23

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 (161) hide show
  1. package/dashboard/dist/assets/9gFmK3Kr.js +1 -0
  2. package/dashboard/dist/assets/9gFmK3Kr.js.br +0 -0
  3. package/dashboard/dist/assets/9gFmK3Kr.js.gz +0 -0
  4. package/dashboard/dist/assets/{DS79hzMu.js → BrMXbzQ-.js} +2 -2
  5. package/dashboard/dist/assets/BrMXbzQ-.js.br +0 -0
  6. package/dashboard/dist/assets/BrMXbzQ-.js.gz +0 -0
  7. package/dashboard/dist/assets/By0MIBj_.js +1 -0
  8. package/dashboard/dist/assets/By0MIBj_.js.br +0 -0
  9. package/dashboard/dist/assets/By0MIBj_.js.gz +0 -0
  10. package/dashboard/dist/assets/C1u2SGin.css +1 -0
  11. package/dashboard/dist/assets/C1u2SGin.css.br +0 -0
  12. package/dashboard/dist/assets/C1u2SGin.css.gz +0 -0
  13. package/dashboard/dist/assets/{467jKHFJ.js → CGJiHCIx.js} +1 -1
  14. package/dashboard/dist/assets/CGJiHCIx.js.br +0 -0
  15. package/dashboard/dist/assets/CGJiHCIx.js.gz +0 -0
  16. package/dashboard/dist/assets/CSd4rSuU.js +212 -0
  17. package/dashboard/dist/assets/CSd4rSuU.js.br +0 -0
  18. package/dashboard/dist/assets/CSd4rSuU.js.gz +0 -0
  19. package/dashboard/dist/assets/{5Ihga-4X.js → CZXS5i_5.js} +1 -1
  20. package/dashboard/dist/assets/CZXS5i_5.js.br +0 -0
  21. package/dashboard/dist/assets/CZXS5i_5.js.gz +0 -0
  22. package/dashboard/dist/assets/{a6qcPiWt.js → CbVWL74-.js} +1 -1
  23. package/dashboard/dist/assets/CbVWL74-.js.br +0 -0
  24. package/dashboard/dist/assets/CbVWL74-.js.gz +0 -0
  25. package/dashboard/dist/assets/{qDJ6rqcs.js → D-FuHfT8.js} +1 -1
  26. package/dashboard/dist/assets/D-FuHfT8.js.br +0 -0
  27. package/dashboard/dist/assets/D-FuHfT8.js.gz +0 -0
  28. package/dashboard/dist/assets/{BcJmNILk.js → D0PN5_vY.js} +1 -1
  29. package/dashboard/dist/assets/D0PN5_vY.js.br +0 -0
  30. package/dashboard/dist/assets/D0PN5_vY.js.gz +0 -0
  31. package/dashboard/dist/assets/DDCPrZRt.js +1 -0
  32. package/dashboard/dist/assets/DDCPrZRt.js.br +0 -0
  33. package/dashboard/dist/assets/DDCPrZRt.js.gz +0 -0
  34. package/dashboard/dist/assets/{B71dt9yu.js → DNQ-iFO2.js} +1 -1
  35. package/dashboard/dist/assets/DNQ-iFO2.js.br +0 -0
  36. package/dashboard/dist/assets/DNQ-iFO2.js.gz +0 -0
  37. package/dashboard/dist/assets/{PVi0vr9a.js → DhPuHPK7.js} +1 -1
  38. package/dashboard/dist/assets/DhPuHPK7.js.br +0 -0
  39. package/dashboard/dist/assets/DhPuHPK7.js.gz +0 -0
  40. package/dashboard/dist/assets/Dhz7qPtn.js +1 -0
  41. package/dashboard/dist/assets/Dhz7qPtn.js.br +0 -0
  42. package/dashboard/dist/assets/Dhz7qPtn.js.gz +0 -0
  43. package/dashboard/dist/assets/LOFrVoPD.js +1 -0
  44. package/dashboard/dist/assets/LOFrVoPD.js.br +0 -0
  45. package/dashboard/dist/assets/LOFrVoPD.js.gz +0 -0
  46. package/dashboard/dist/assets/OlLPtzdz.js +1 -0
  47. package/dashboard/dist/assets/OlLPtzdz.js.br +0 -0
  48. package/dashboard/dist/assets/OlLPtzdz.js.gz +0 -0
  49. package/dashboard/dist/assets/{sdoPH_Z1.js → RN4M9u9W.js} +2 -2
  50. package/dashboard/dist/assets/RN4M9u9W.js.br +0 -0
  51. package/dashboard/dist/assets/RN4M9u9W.js.gz +0 -0
  52. package/dashboard/dist/assets/VCHu272d.js +1 -0
  53. package/dashboard/dist/assets/VCHu272d.js.br +0 -0
  54. package/dashboard/dist/assets/VCHu272d.js.gz +0 -0
  55. package/dashboard/dist/assets/m2smti3F.js +1 -0
  56. package/dashboard/dist/assets/m2smti3F.js.br +0 -0
  57. package/dashboard/dist/assets/m2smti3F.js.gz +0 -0
  58. package/dashboard/dist/assets/{C3_j_W9V.js → nra1yvJX.js} +1 -1
  59. package/dashboard/dist/assets/nra1yvJX.js.br +0 -0
  60. package/dashboard/dist/assets/nra1yvJX.js.gz +0 -0
  61. package/dashboard/dist/assets/qLX6NZ-J.js +1 -0
  62. package/dashboard/dist/assets/qLX6NZ-J.js.br +0 -0
  63. package/dashboard/dist/assets/qLX6NZ-J.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/agent-run-store.js +162 -24
  68. package/dist/cli/orgx.d.ts +3 -0
  69. package/dist/config/resolution.d.ts +7 -0
  70. package/dist/config/resolution.js +13 -5
  71. package/dist/contracts/onboarding-state.d.ts +2 -0
  72. package/dist/contracts/onboarding-state.js +23 -0
  73. package/dist/contracts/shared-types.d.ts +17 -0
  74. package/dist/http/helpers/auto-continue-engine.d.ts +62 -0
  75. package/dist/http/helpers/auto-continue-engine.js +329 -53
  76. package/dist/http/helpers/autopilot-runtime.js +5 -1
  77. package/dist/http/helpers/autopilot-slice-utils.js +25 -1
  78. package/dist/http/helpers/decision-mapper.d.ts +1 -0
  79. package/dist/http/helpers/decision-mapper.js +19 -2
  80. package/dist/http/helpers/dispatch-lifecycle.js +3 -0
  81. package/dist/http/helpers/mission-control.d.ts +1 -0
  82. package/dist/http/helpers/mission-control.js +5 -2
  83. package/dist/http/helpers/slice-run-projections.d.ts +27 -0
  84. package/dist/http/helpers/slice-run-projections.js +198 -10
  85. package/dist/http/helpers/triage-mapper.js +220 -6
  86. package/dist/http/index.d.ts +1 -0
  87. package/dist/http/index.js +94 -46
  88. package/dist/http/router.js +64 -9
  89. package/dist/http/routes/live-legacy.d.ts +19 -2
  90. package/dist/http/routes/live-legacy.js +110 -27
  91. package/dist/http/routes/live-snapshot.d.ts +16 -2
  92. package/dist/http/routes/live-snapshot.js +169 -25
  93. package/dist/http/routes/mission-control-actions.js +28 -0
  94. package/dist/http/routes/mission-control-read.d.ts +18 -0
  95. package/dist/http/routes/mission-control-read.js +130 -218
  96. package/dist/http/routes/onboarding.d.ts +1 -0
  97. package/dist/http/routes/onboarding.js +17 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.js +199 -123
  100. package/dist/outbox.d.ts +0 -2
  101. package/dist/outbox.js +268 -150
  102. package/dist/reporting/rollups.js +18 -11
  103. package/dist/runtime-instance-store.js +212 -58
  104. package/dist/stores/materialized-snapshot-store.d.ts +18 -0
  105. package/dist/stores/materialized-snapshot-store.js +91 -0
  106. package/dist/stores/sqlite-state.d.ts +6 -0
  107. package/dist/stores/sqlite-state.js +179 -0
  108. package/package.json +6 -1
  109. package/dashboard/dist/assets/467jKHFJ.js.br +0 -0
  110. package/dashboard/dist/assets/467jKHFJ.js.gz +0 -0
  111. package/dashboard/dist/assets/5Ihga-4X.js.br +0 -0
  112. package/dashboard/dist/assets/5Ihga-4X.js.gz +0 -0
  113. package/dashboard/dist/assets/B71dt9yu.js.br +0 -0
  114. package/dashboard/dist/assets/B71dt9yu.js.gz +0 -0
  115. package/dashboard/dist/assets/BCudUvwg.js +0 -1
  116. package/dashboard/dist/assets/BCudUvwg.js.br +0 -0
  117. package/dashboard/dist/assets/BCudUvwg.js.gz +0 -0
  118. package/dashboard/dist/assets/BEnI6kNR.js +0 -1
  119. package/dashboard/dist/assets/BEnI6kNR.js.br +0 -0
  120. package/dashboard/dist/assets/BEnI6kNR.js.gz +0 -0
  121. package/dashboard/dist/assets/BcJmNILk.js.br +0 -0
  122. package/dashboard/dist/assets/BcJmNILk.js.gz +0 -0
  123. package/dashboard/dist/assets/C-MOJWHs.js +0 -1
  124. package/dashboard/dist/assets/C-MOJWHs.js.br +0 -0
  125. package/dashboard/dist/assets/C-MOJWHs.js.gz +0 -0
  126. package/dashboard/dist/assets/C-XuWXGi.js +0 -1
  127. package/dashboard/dist/assets/C-XuWXGi.js.br +0 -0
  128. package/dashboard/dist/assets/C-XuWXGi.js.gz +0 -0
  129. package/dashboard/dist/assets/C3_j_W9V.js.br +0 -0
  130. package/dashboard/dist/assets/C3_j_W9V.js.gz +0 -0
  131. package/dashboard/dist/assets/C9-UYhBb.js +0 -1
  132. package/dashboard/dist/assets/C9-UYhBb.js.br +0 -0
  133. package/dashboard/dist/assets/C9-UYhBb.js.gz +0 -0
  134. package/dashboard/dist/assets/C9yV06GS.js +0 -1
  135. package/dashboard/dist/assets/C9yV06GS.js.br +0 -0
  136. package/dashboard/dist/assets/C9yV06GS.js.gz +0 -0
  137. package/dashboard/dist/assets/CReugbyT.js +0 -1
  138. package/dashboard/dist/assets/CReugbyT.js.br +0 -0
  139. package/dashboard/dist/assets/CReugbyT.js.gz +0 -0
  140. package/dashboard/dist/assets/CSDhTbKy.js +0 -1
  141. package/dashboard/dist/assets/CSDhTbKy.js.br +0 -0
  142. package/dashboard/dist/assets/CSDhTbKy.js.gz +0 -0
  143. package/dashboard/dist/assets/CfMS9yIf.js +0 -1
  144. package/dashboard/dist/assets/CfMS9yIf.js.br +0 -0
  145. package/dashboard/dist/assets/CfMS9yIf.js.gz +0 -0
  146. package/dashboard/dist/assets/D2Kqcmv9.js +0 -212
  147. package/dashboard/dist/assets/D2Kqcmv9.js.br +0 -0
  148. package/dashboard/dist/assets/D2Kqcmv9.js.gz +0 -0
  149. package/dashboard/dist/assets/DS79hzMu.js.br +0 -0
  150. package/dashboard/dist/assets/DS79hzMu.js.gz +0 -0
  151. package/dashboard/dist/assets/PVi0vr9a.js.br +0 -0
  152. package/dashboard/dist/assets/PVi0vr9a.js.gz +0 -0
  153. package/dashboard/dist/assets/RZkbqlJk.css +0 -1
  154. package/dashboard/dist/assets/RZkbqlJk.css.br +0 -0
  155. package/dashboard/dist/assets/RZkbqlJk.css.gz +0 -0
  156. package/dashboard/dist/assets/a6qcPiWt.js.br +0 -0
  157. package/dashboard/dist/assets/a6qcPiWt.js.gz +0 -0
  158. package/dashboard/dist/assets/qDJ6rqcs.js.br +0 -0
  159. package/dashboard/dist/assets/qDJ6rqcs.js.gz +0 -0
  160. package/dashboard/dist/assets/sdoPH_Z1.js.br +0 -0
  161. package/dashboard/dist/assets/sdoPH_Z1.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();
@@ -114,15 +87,43 @@ async function appendOutboxDeadLetterRecord(sessionId, record) {
114
87
  await hardenPath(targetPath, 0o600);
115
88
  }
116
89
  export async function appendOutboxDeadLetter(sessionId, event, reason, error) {
90
+ let normalizedSessionId;
91
+ try {
92
+ normalizedSessionId = normalizeSessionId(sessionId);
93
+ }
94
+ catch {
95
+ return;
96
+ }
117
97
  const droppedAt = new Date().toISOString();
118
- await appendOutboxDeadLetterRecord(sessionId, {
98
+ await appendOutboxDeadLetterRecord(normalizedSessionId, {
119
99
  droppedAt,
120
- queueId: normalizeSessionId(sessionId),
100
+ queueId: normalizedSessionId,
121
101
  reason,
122
102
  error: error ?? null,
123
103
  event,
124
104
  });
125
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
+ }
126
127
  /** Drop stale events and enforce per-session cap. Returns true if any were removed. */
127
128
  function pruneOutboxEvents(events) {
128
129
  const cutoff = Date.now() - OUTBOX_EVENT_TTL_MS;
@@ -130,50 +131,67 @@ function pruneOutboxEvents(events) {
130
131
  const epoch = Date.parse(e.timestamp);
131
132
  return Number.isFinite(epoch) && epoch >= cutoff;
132
133
  });
133
- // Keep newest events if over cap.
134
134
  const capped = fresh.length > OUTBOX_MAX_EVENTS_PER_SESSION
135
135
  ? fresh.slice(fresh.length - OUTBOX_MAX_EVENTS_PER_SESSION)
136
136
  : fresh;
137
137
  return { pruned: capped, changed: capped.length !== events.length };
138
138
  }
139
- export async function readOutbox(sessionId) {
140
- let targetPath;
141
- try {
142
- targetPath = outboxPath(sessionId);
143
- }
144
- catch {
145
- return [];
146
- }
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);
147
189
  try {
148
190
  const raw = await readFile(targetPath, "utf8");
149
191
  try {
150
192
  const parsed = JSON.parse(raw);
151
193
  const events = Array.isArray(parsed) ? parsed : [];
152
- const { pruned, changed } = pruneOutboxEvents(events);
153
- const filtered = [];
154
- let filteredChanged = changed;
155
- for (const event of pruned) {
156
- const skipReason = classifyOutboxReplaySkip(event);
157
- if (!skipReason) {
158
- filtered.push(event);
159
- continue;
160
- }
161
- filteredChanged = true;
162
- await appendOutboxDeadLetter(sessionId, event, `pruned_on_read:${skipReason}`, null);
163
- }
164
- // Write back if stale events were dropped.
165
- if (filteredChanged) {
166
- if (filtered.length === 0) {
167
- try {
168
- await unlink(targetPath);
169
- }
170
- catch { /* ok */ }
171
- }
172
- else {
173
- await writeFileAtomic(targetPath, JSON.stringify(filtered), 0o600);
174
- }
175
- }
176
- return filtered;
194
+ return events.map((event) => normalizeOutboxEvent(event));
177
195
  }
178
196
  catch {
179
197
  await backupCorruptOutboxFile(targetPath);
@@ -184,6 +202,103 @@ export async function readOutbox(sessionId) {
184
202
  return [];
185
203
  }
186
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
+ }
187
302
  export async function appendToOutbox(sessionId, event) {
188
303
  let normalizedSessionId;
189
304
  try {
@@ -192,22 +307,39 @@ export async function appendToOutbox(sessionId, event) {
192
307
  catch {
193
308
  return;
194
309
  }
195
- const skipReason = classifyOutboxReplaySkip(event);
310
+ const normalizedEvent = normalizeOutboxEvent(event);
311
+ const skipReason = classifyOutboxReplaySkip(normalizedEvent);
196
312
  if (skipReason) {
197
- await appendOutboxDeadLetter(normalizedSessionId, event, `suppressed_on_append:${skipReason}`, null);
313
+ await appendOutboxDeadLetter(normalizedSessionId, normalizedEvent, `suppressed_on_append:${skipReason}`, null);
198
314
  return;
199
315
  }
200
- await ensureDir();
201
- const targetPath = outboxPath(normalizedSessionId);
202
- const existing = await readOutbox(normalizedSessionId);
203
- const idx = existing.findIndex((item) => item.id === event.id);
204
- if (idx >= 0) {
205
- existing[idx] = event;
206
- }
207
- else {
208
- existing.push(event);
209
- }
210
- 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);
211
343
  }
212
344
  export async function replaceOutbox(sessionId, events) {
213
345
  let normalizedSessionId;
@@ -217,93 +349,75 @@ export async function replaceOutbox(sessionId, events) {
217
349
  catch {
218
350
  return;
219
351
  }
220
- await ensureDir();
221
- const targetPath = outboxPath(normalizedSessionId);
352
+ await ensureOutboxMigrated();
222
353
  if (events.length === 0) {
354
+ getStateDb()
355
+ .prepare("DELETE FROM outbox_events WHERE queue_id = ?")
356
+ .run(normalizedSessionId);
223
357
  try {
224
- await unlink(targetPath);
225
- return;
358
+ await unlink(outboxPath(normalizedSessionId));
226
359
  }
227
360
  catch {
228
361
  // File may not exist
229
- return;
230
362
  }
363
+ return;
231
364
  }
232
- await writeFileAtomic(targetPath, JSON.stringify(events), 0o600);
365
+ writeOutboxEvents(normalizedSessionId, events);
366
+ pruneQueueRows(normalizedSessionId);
233
367
  }
234
368
  export async function readAllOutboxItems() {
235
- try {
236
- await ensureDir();
237
- const files = await readdir(outboxDir());
238
- const items = [];
239
- for (const file of files) {
240
- if (!file.endsWith(".json"))
241
- continue;
242
- const sessionId = file.slice(0, -5);
243
- try {
244
- const events = await readOutbox(sessionId);
245
- for (const evt of events) {
246
- items.push(evt.activityItem);
247
- }
248
- }
249
- catch {
250
- // 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);
251
382
  }
252
383
  }
253
- return items.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
254
- }
255
- catch {
256
- return [];
384
+ catch {
385
+ // skip malformed rows
386
+ }
257
387
  }
388
+ return items;
258
389
  }
259
390
  export async function readOutboxSummary() {
260
- try {
261
- await ensureDir();
262
- const files = await readdir(outboxDir());
263
- const pendingByQueue = {};
264
- let pendingTotal = 0;
265
- let oldestEpoch = Number.POSITIVE_INFINITY;
266
- let newestEpoch = Number.NEGATIVE_INFINITY;
267
- for (const file of files) {
268
- if (!file.endsWith(".json"))
269
- continue;
270
- const queueId = file.slice(0, -5);
271
- try {
272
- const events = await readOutbox(queueId);
273
- const count = Array.isArray(events) ? events.length : 0;
274
- pendingByQueue[queueId] = count;
275
- pendingTotal += count;
276
- for (const event of events) {
277
- const epoch = Date.parse(event.timestamp);
278
- if (!Number.isFinite(epoch))
279
- continue;
280
- oldestEpoch = Math.min(oldestEpoch, epoch);
281
- newestEpoch = Math.max(newestEpoch, epoch);
282
- }
283
- }
284
- catch {
285
- pendingByQueue[queueId] = pendingByQueue[queueId] ?? 0;
286
- }
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;
287
413
  }
288
- return {
289
- pendingTotal,
290
- pendingByQueue,
291
- oldestEventAt: Number.isFinite(oldestEpoch)
292
- ? new Date(oldestEpoch).toISOString()
293
- : null,
294
- newestEventAt: Number.isFinite(newestEpoch)
295
- ? new Date(newestEpoch).toISOString()
296
- : null,
297
- };
298
- }
299
- catch {
300
- return {
301
- pendingTotal: 0,
302
- pendingByQueue: {},
303
- oldestEventAt: null,
304
- newestEventAt: null,
305
- };
306
414
  }
415
+ return {
416
+ pendingTotal,
417
+ pendingByQueue,
418
+ oldestEventAt,
419
+ newestEventAt,
420
+ };
307
421
  }
308
422
  export async function clearOutbox(sessionId) {
309
423
  let normalizedSessionId;
@@ -313,6 +427,10 @@ export async function clearOutbox(sessionId) {
313
427
  catch {
314
428
  return;
315
429
  }
430
+ await ensureOutboxMigrated();
431
+ getStateDb()
432
+ .prepare("DELETE FROM outbox_events WHERE queue_id = ?")
433
+ .run(normalizedSessionId);
316
434
  try {
317
435
  await unlink(outboxPath(normalizedSessionId));
318
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";