mixdog 0.9.25 → 0.9.27

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 (87) hide show
  1. package/package.json +2 -1
  2. package/scripts/arg-guard-test.mjs +68 -0
  3. package/scripts/compacted-placeholder-scrub-test.mjs +77 -0
  4. package/scripts/steering-fold-provenance-test.mjs +71 -0
  5. package/scripts/webhook-smoke.mjs +46 -53
  6. package/src/cli.mjs +40 -4
  7. package/src/defaults/skills/setup/SKILL.md +6 -1
  8. package/src/rules/shared/01-tool.md +11 -4
  9. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  10. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +17 -0
  11. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +28 -10
  13. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +8 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  15. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +51 -15
  16. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +5 -1
  17. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +164 -9
  18. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +75 -0
  19. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +5 -0
  20. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +4 -0
  21. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +12 -1
  22. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +14 -1
  23. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +7 -3
  28. package/src/runtime/channels/lib/config.mjs +13 -11
  29. package/src/runtime/channels/lib/memory-client.mjs +22 -2
  30. package/src/runtime/channels/lib/owned-runtime.mjs +1 -1
  31. package/src/runtime/channels/lib/scheduler.mjs +226 -208
  32. package/src/runtime/channels/lib/status-snapshot.mjs +16 -0
  33. package/src/runtime/channels/lib/webhook/deliveries.mjs +7 -318
  34. package/src/runtime/channels/lib/webhook.mjs +98 -150
  35. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  36. package/src/runtime/memory/index.mjs +50 -25
  37. package/src/runtime/memory/lib/cycle-scheduler.mjs +3 -1
  38. package/src/runtime/memory/lib/memory-config-flags.mjs +13 -1
  39. package/src/runtime/memory/lib/pg/adapter.mjs +6 -1
  40. package/src/runtime/memory/lib/pg/process.mjs +19 -1
  41. package/src/runtime/memory/lib/pg/supervisor.mjs +125 -14
  42. package/src/runtime/memory/lib/query-handlers.mjs +102 -0
  43. package/src/runtime/memory/lib/recall-format.mjs +60 -22
  44. package/src/runtime/memory/lib/session-ingest-runtime.mjs +20 -6
  45. package/src/runtime/memory/tool-defs.mjs +3 -3
  46. package/src/runtime/shared/child-guardian.mjs +2 -2
  47. package/src/runtime/shared/open-url.mjs +2 -1
  48. package/src/runtime/shared/schedules-db.mjs +350 -0
  49. package/src/runtime/shared/service-discovery.mjs +169 -0
  50. package/src/runtime/shared/spawn-flags.mjs +27 -0
  51. package/src/runtime/shared/staged-install-worker.mjs +14 -0
  52. package/src/runtime/shared/staged-update.mjs +530 -0
  53. package/src/runtime/shared/tool-primitives.mjs +3 -1
  54. package/src/runtime/shared/tool-surface.mjs +19 -3
  55. package/src/runtime/shared/update-checker.mjs +54 -10
  56. package/src/runtime/shared/webhooks-db.mjs +405 -0
  57. package/src/session-runtime/channel-config-api.mjs +13 -13
  58. package/src/session-runtime/lifecycle-api.mjs +12 -0
  59. package/src/session-runtime/runtime-core.mjs +41 -23
  60. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  61. package/src/standalone/agent-tool.mjs +42 -11
  62. package/src/standalone/channel-admin.mjs +173 -121
  63. package/src/standalone/channel-worker.mjs +2 -2
  64. package/src/standalone/memory-runtime-proxy.mjs +10 -5
  65. package/src/tui/App.jsx +32 -10
  66. package/src/tui/app/channel-pickers.mjs +9 -9
  67. package/src/tui/app/clipboard.mjs +14 -0
  68. package/src/tui/app/doctor.mjs +1 -1
  69. package/src/tui/app/maintenance-pickers.mjs +17 -7
  70. package/src/tui/app/settings-picker.mjs +2 -2
  71. package/src/tui/app/transcript-window.mjs +63 -7
  72. package/src/tui/app/use-mouse-input.mjs +19 -6
  73. package/src/tui/components/Spinner.jsx +5 -2
  74. package/src/tui/components/StatusLine.jsx +7 -7
  75. package/src/tui/components/ToolExecution.jsx +36 -62
  76. package/src/tui/display-width.mjs +18 -8
  77. package/src/tui/dist/index.mjs +502 -324
  78. package/src/tui/engine/session-api-ext.mjs +12 -12
  79. package/src/tui/hooks/useSharedTick.mjs +103 -0
  80. package/src/tui/index.jsx +12 -2
  81. package/src/tui/markdown/format-token.mjs +46 -8
  82. package/src/tui/markdown/render-ansi.mjs +24 -1
  83. package/src/tui/markdown/stream-fence.mjs +60 -0
  84. package/src/tui/markdown/streaming-markdown.mjs +22 -1
  85. package/src/ui/statusline-segments.mjs +12 -1
  86. package/vendor/ink/build/display-width.js +10 -8
  87. package/src/runtime/shared/schedules-store.mjs +0 -82
@@ -1,318 +1,16 @@
1
- import { join } from "path";
2
- import { readFileSync, mkdirSync, writeFileSync, unlinkSync, existsSync, renameSync, watch as fsWatch } from "fs";
3
- import { appendFile } from "fs/promises";
4
- import { DATA_DIR } from "../config.mjs";
5
- import { readMarkdownDocument } from "../../../shared/markdown-frontmatter.mjs";
6
- import { logWebhook } from "./log.mjs";
1
+ // Header helpers retained after the webhook store moved to PG
2
+ // (src/runtime/shared/webhooks-db.mjs). The file-based endpoint config,
3
+ // secret side-file, deliveries.jsonl log and the fs.watch cache are retired
4
+ // from the serving path; only these two pure functions are still consumed by
5
+ // webhook.mjs (delivery-id extraction + the diagnostic headers summary).
7
6
 
8
- const WEBHOOKS_DIR = join(DATA_DIR, "webhooks");
9
-
10
- // ── Endpoint config loader ─────────────────────────────────────────────
11
- // Reads DATA_DIR/webhooks/<name>/WEBHOOK.md (written by setup-server.mjs
12
- // via POST /webhooks). The frontmatter IS the config object; the markdown
13
- // body is the instructions/prompt. Cached in-memory, invalidated by
14
- // fs.watch on the webhooks directory. Returns the frontmatter object
15
- // { secret, parser, channel, model, role, enabled } where routing is by
16
- // `channel` presence and `role` names a user-workflow entry when set.
17
- const _endpointCache = new Map();
18
- let _endpointWatcher = null;
19
- function _endpointConfigPath(name) {
20
- return join(WEBHOOKS_DIR, name, "WEBHOOK.md");
21
- }
22
- // Per-endpoint HMAC secret is stored in a side file (WEBHOOKS_DIR/<name>/secret),
23
- // not in WEBHOOK.md frontmatter — frontmatter is a lossy `key: value` format
24
- // (unquote strips surrounding quotes) and would corrupt user secrets on the
25
- // save->rewrite round-trip. Read fresh on each verify (no cache): the signing
26
- // path is not hot and a stale secret would silently reject valid deliveries.
27
- function _readEndpointSecret(name) {
28
- try {
29
- const s = readFileSync(join(WEBHOOKS_DIR, name, "secret"), "utf8").trim();
30
- return s || null;
31
- } catch {
32
- return null;
33
- }
34
- }
35
- function _ensureEndpointWatcher() {
36
- if (_endpointWatcher) return;
37
- try {
38
- if (!existsSync(WEBHOOKS_DIR)) return;
39
- _endpointWatcher = fsWatch(WEBHOOKS_DIR, { recursive: true }, (_event, filename) => {
40
- if (!filename) { _endpointCache.clear(); return; }
41
- // filename is like "<endpoint>/WEBHOOK.md" or "<endpoint>"
42
- const parts = String(filename).split(/[\\/]/);
43
- const endpointName = parts[0];
44
- if (endpointName) _endpointCache.delete(endpointName);
45
- else _endpointCache.clear();
46
- });
47
- _endpointWatcher.on("error", () => { _endpointWatcher = null; _endpointCache.clear(); });
48
- // Don't let the watcher keep the event loop alive: embedders/tests that
49
- // merely load an endpoint config should still be able to exit cleanly.
50
- _endpointWatcher.unref?.();
51
- } catch {
52
- // Watch failures are non-fatal; cache simply stays until process restart.
53
- }
54
- }
55
- function _closeEndpointWatcher() {
56
- if (!_endpointWatcher) return;
57
- try { _endpointWatcher.close(); } catch {}
58
- _endpointWatcher = null;
59
- _endpointCache.clear();
60
- }
61
- function loadEndpointConfig(name) {
62
- if (!name) return null;
63
- // A cached entry is only authoritative while the fs.watch handle is
64
- // armed — otherwise a later mkdir+write of WEBHOOKS_DIR/<name>/
65
- // WEBHOOK.md has no way to invalidate the cache and a stale `null`
66
- // (e.g. captured before WEBHOOKS_DIR existed) would pin forever.
67
- if (_endpointCache.has(name) && _endpointWatcher) return _endpointCache.get(name);
68
- _ensureEndpointWatcher();
69
- const p = _endpointConfigPath(name);
70
- if (!existsSync(p)) {
71
- // Only cache the missing-config state when the watcher is live, so
72
- // a later create can invalidate it. Otherwise leave the slot empty
73
- // and re-read on the next call.
74
- if (_endpointWatcher) _endpointCache.set(name, null);
75
- return null;
76
- }
77
- try {
78
- // Frontmatter is the config object; `enabled` arrives as a string and
79
- // is cast so `endpoint?.enabled === false` gates match a written
80
- // `enabled: false`.
81
- const { frontmatter } = readMarkdownDocument(readFileSync(p, "utf8"));
82
- const cfg = { ...frontmatter };
83
- if (Object.prototype.hasOwnProperty.call(cfg, "enabled")) {
84
- cfg.enabled = cfg.enabled !== "false" && cfg.enabled !== false;
85
- }
86
- _endpointCache.set(name, cfg);
87
- return cfg;
88
- } catch {
89
- if (_endpointWatcher) _endpointCache.set(name, null);
90
- return null;
91
- }
92
- }
93
-
94
- // ── Delivery tracking ─────────────────────────────────────────────────
95
- // Per-endpoint append-only log at WEBHOOKS_DIR/<name>/deliveries.jsonl.
96
- // Each POST writes at least two lines: {status:"pending"|"processing"}
97
- // then {status:"done"|"failed"|"dedup"}. Earlier fields (payloadPreview,
98
- // headersSummary) are kept on the first line only; later status updates
99
- // reference the same `id` and are merged latest-wins at read time.
100
- const DELIVERY_INDEX_MAX_IDS = 2000;
101
- const DELIVERY_LOG_MAX_LINES = 10_000;
102
- // A "pending" claim normally resolves to a terminal status (done/failed) or
103
- // "processing" within seconds. If a row is stuck at "pending" past this TTL
104
- // (crash between the pending write and the terminal write, with no restart
105
- // warm to clear it), treat it as abandoned rather than always-kept —
106
- // otherwise an unbounded number of stuck-pending ids could accumulate and
107
- // defeat DELIVERY_INDEX_MAX_IDS entirely.
108
- const DELIVERY_PENDING_TTL_MS = 10 * 60 * 1000;
109
- function _isFreshPending(entry) {
110
- const ts = Date.parse(entry?.ts ?? "");
111
- if (Number.isNaN(ts)) return true; // no timestamp to judge staleness — keep (fail safe toward dedup).
112
- return Date.now() - ts < DELIVERY_PENDING_TTL_MS;
113
- }
114
- /** @type {Map<string, Map<string, object>>} */
115
- const _deliveryIndexByEndpoint = new Map();
116
- /** @type {Set<string>} */
117
- const _deliveryIndexWarmed = new Set();
118
- /** @type {Map<string, number>} */
119
- const _deliveryLogLineCountByEndpoint = new Map();
120
- /** @type {Map<string, number>} distinct-id count at last warm/compaction; drives the redundancy-based compaction trigger. */
121
- const _deliveryKeptCountByEndpoint = new Map();
122
- function _deliveriesPath(name) {
123
- return join(WEBHOOKS_DIR, name, "deliveries.jsonl");
124
- }
125
- function _mergeDeliveryRows(prior, entry) {
126
- return prior ? { ...prior, ...entry } : entry;
127
- }
128
- function _isBlockingDeliveryStatus(status) {
129
- // "pending" is a non-terminal claim too: between the pending row write and
130
- // the terminal done/failed row (eventPipeline enqueue, delegate dispatch) a
131
- // concurrent retry of the same delivery id must dedup, not double-dispatch.
132
- return status === "received" || status === "pending" || status === "processing" || status === "done";
133
- }
134
- function _deliveryIndexFor(name) {
135
- let map = _deliveryIndexByEndpoint.get(name);
136
- if (!map) {
137
- map = new Map();
138
- _deliveryIndexByEndpoint.set(name, map);
139
- }
140
- return map;
141
- }
142
- // Bound retained ids so successful ("done") deliveries cannot accumulate
143
- // forever in RAM or on disk. In-flight claims (received/processing) are
144
- // ALWAYS kept — dropping one would let a duplicate dispatch through. The
145
- // remaining DELIVERY_INDEX_MAX_IDS budget goes to the newest "done" rows
146
- // (dedup of recent retries) first, then newest terminal rows for history.
147
- // Older "done" rows age out; a sender re-delivering an id that stale is
148
- // treated as new — acceptable beyond any realistic retry window.
149
- function _retainedDeliveryIds(entries) {
150
- const inflight = [];
151
- const done = [];
152
- const other = [];
153
- for (const e of entries) {
154
- // "pending" is a non-terminal claim too (see _isBlockingDeliveryStatus)
155
- // — it must stay in the always-kept inflight set during pruning, or a
156
- // concurrent retry of the same id can bypass dedup while the original
157
- // delivery is still pending. Bounded by DELIVERY_PENDING_TTL_MS so a
158
- // stuck/abandoned pending row (crash, never resolved) doesn't pin
159
- // itself in the always-kept set forever and grow the index unbounded;
160
- // once stale it falls through to the capped `other` pool below.
161
- if (e.status === "received" || e.status === "processing" || (e.status === "pending" && _isFreshPending(e))) {
162
- inflight.push(e);
163
- }
164
- else if (e.status === "done") done.push(e);
165
- else other.push(e);
166
- }
167
- const keep = new Set(inflight.map((e) => e.id));
168
- const byTsDesc = (a, b) => String(b.ts || "").localeCompare(String(a.ts || ""));
169
- done.sort(byTsDesc);
170
- other.sort(byTsDesc);
171
- for (const e of [...done, ...other]) {
172
- if (keep.size >= DELIVERY_INDEX_MAX_IDS) break;
173
- keep.add(e.id);
174
- }
175
- return keep;
176
- }
177
- function _pruneDeliveryIndexMap(byId) {
178
- if (byId.size <= DELIVERY_INDEX_MAX_IDS) return;
179
- const keep = _retainedDeliveryIds([...byId.values()]);
180
- for (const id of byId.keys()) {
181
- if (!keep.has(id)) byId.delete(id);
182
- }
183
- }
184
- function _deliveryLogLineCount(name) {
185
- return _deliveryLogLineCountByEndpoint.get(name) ?? 0;
186
- }
187
- function _setDeliveryLogLineCount(name, n) {
188
- _deliveryLogLineCountByEndpoint.set(name, Math.max(0, n));
189
- }
190
- function _bumpDeliveryLogLineCount(name, delta = 1) {
191
- _setDeliveryLogLineCount(name, _deliveryLogLineCount(name) + delta);
192
- }
193
- function _readDeliveriesFileMerged(name) {
194
- const p = _deliveriesPath(name);
195
- const byId = new Map();
196
- let lineCount = 0;
197
- if (!existsSync(p)) return { byId, lineCount };
198
- try {
199
- const raw = readFileSync(p, "utf8");
200
- for (const line of raw.split("\n")) {
201
- if (!line) continue;
202
- lineCount++;
203
- try {
204
- const entry = JSON.parse(line);
205
- if (!entry?.id) continue;
206
- byId.set(entry.id, _mergeDeliveryRows(byId.get(entry.id), entry));
207
- } catch {}
208
- }
209
- } catch {}
210
- return { byId, lineCount };
211
- }
212
- function _ingestDeliveriesFileIntoIndex(name) {
213
- const { byId: merged, lineCount } = _readDeliveriesFileMerged(name);
214
- const byId = _deliveryIndexFor(name);
215
- byId.clear();
216
- for (const [id, row] of merged) byId.set(id, row);
217
- _pruneDeliveryIndexMap(byId);
218
- _setDeliveryLogLineCount(name, lineCount);
219
- // Track the RETAINED (post-prune) distinct count, not the raw file count:
220
- // a pre-existing oversized log then trips the compaction trigger promptly
221
- // instead of inflating the threshold until it grows even larger.
222
- _deliveryKeptCountByEndpoint.set(name, byId.size);
223
- }
224
- function _ensureDeliveryIndex(name) {
225
- if (_deliveryIndexWarmed.has(name)) return;
226
- _deliveryIndexWarmed.add(name);
227
- _ingestDeliveriesFileIntoIndex(name);
228
- }
229
- function _applyDeliveryEntryToIndex(name, entry) {
230
- if (!entry?.id) return;
231
- const byId = _deliveryIndexFor(name);
232
- byId.set(entry.id, _mergeDeliveryRows(byId.get(entry.id), entry));
233
- _pruneDeliveryIndexMap(byId);
234
- }
235
- function _compactDeliveriesLogIfNeeded(name) {
236
- // Redundancy-based trigger: compact only when the log holds meaningfully more
237
- // lines than distinct ids (i.e. there are status-update rows to collapse).
238
- // The threshold scales with the distinct-id count so an endpoint with many
239
- // legitimate blocking ids does NOT re-compact on every append (which would
240
- // re-read the whole log permanently once distinct > DELIVERY_LOG_MAX_LINES).
241
- const kept = _deliveryKeptCountByEndpoint.get(name) ?? _deliveryIndexFor(name).size;
242
- const threshold = Math.max(DELIVERY_LOG_MAX_LINES, kept * 2);
243
- if (_deliveryLogLineCount(name) <= threshold) return;
244
- const { byId: merged } = _readDeliveriesFileMerged(name);
245
- const rows = [...merged.values()];
246
- const keepIds = _retainedDeliveryIds(rows);
247
- const keep = new Map();
248
- for (const e of rows) {
249
- if (keepIds.has(e.id)) keep.set(e.id, e);
250
- }
251
- const lines = [...keep.values()]
252
- .sort((a, b) => String(a.ts || "").localeCompare(String(b.ts || "")))
253
- .map((e) => JSON.stringify(e) + "\n")
254
- .join("");
255
- const p = _deliveriesPath(name);
256
- const tmp = `${p}.compact-${process.pid}-${Date.now()}.tmp`;
257
- try {
258
- writeFileSync(tmp, lines);
259
- renameSync(tmp, p);
260
- // Refresh index + counters by RE-READING the post-rename file (not the
261
- // pre-rename `keep` snapshot). The webhook daemon is the single writer and
262
- // append+compact run synchronously in one process, so no append can
263
- // interleave between the fresh read and the rename; re-reading keeps the
264
- // warmed state exactly matching on-disk content.
265
- _ingestDeliveriesFileIntoIndex(name);
266
- } catch (err) {
267
- logWebhook(`${name}: deliveries compact failed: ${err?.message ?? err}`);
268
- try {
269
- if (existsSync(tmp)) unlinkSync(tmp);
270
- } catch {}
271
- }
272
- }
273
- function appendDelivery(name, entry) {
274
- try {
275
- const dir = join(WEBHOOKS_DIR, name);
276
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
277
- const full = { ts: new Date().toISOString(), ...entry };
278
- const line = JSON.stringify(full) + "\n";
279
- void appendFile(_deliveriesPath(name), line).catch((err) => {
280
- logWebhook(`${name}: deliveries append failed: ${err?.message ?? err}`);
281
- });
282
- const wasWarmed = _deliveryIndexWarmed.has(name);
283
- _ensureDeliveryIndex(name);
284
- if (wasWarmed) _bumpDeliveryLogLineCount(name, 1);
285
- _applyDeliveryEntryToIndex(name, full);
286
- _compactDeliveriesLogIfNeeded(name);
287
- return true;
288
- } catch (err) {
289
- logWebhook(`${name}: deliveries append failed: ${err?.message ?? err}`);
290
- return false;
291
- }
292
- }
293
- // Dedup gate against a still-active claim or a successful prior delivery.
294
- // Only rows with status "received" (non-terminal claim) or "done"
295
- // (successful delivery) block a retry; terminal "failed" rows (and any
296
- // other unknown/legacy terminal status) are NOT considered duplicates so
297
- // a sender can legitimately
298
- // redeliver the same id after a recoverable failure. Without this
299
- // scoping, every prior row would permanently dedup the id and stop
300
- // legit redelivery.
301
- function deliveryExists(name, id) {
302
- // "processing" must also dedup: a delegate dispatch in flight (up to
303
- // DISPATCH_TIMEOUT_MS = 10 min) would otherwise be duplicated by a
304
- // retried delivery of the same id while the first handler is still
305
- // running. Block on any non-terminal status.
306
- _ensureDeliveryIndex(name);
307
- const entry = _deliveryIndexFor(name).get(id);
308
- return Boolean(entry && _isBlockingDeliveryStatus(entry.status));
309
- }
310
7
  function extractDeliveryId(headers) {
311
8
  return headers["x-github-delivery"]
312
9
  || headers["x-delivery-id"]
313
10
  || headers["x-request-id"]
314
11
  || null;
315
12
  }
13
+
316
14
  function buildHeadersSummary(headers) {
317
15
  const summary = {};
318
16
  if (headers["x-github-event"]) summary.event_type = headers["x-github-event"];
@@ -325,13 +23,4 @@ function buildHeadersSummary(headers) {
325
23
  return summary;
326
24
  }
327
25
 
328
- export {
329
- WEBHOOKS_DIR,
330
- _readEndpointSecret,
331
- _closeEndpointWatcher,
332
- loadEndpointConfig,
333
- appendDelivery,
334
- deliveryExists,
335
- extractDeliveryId,
336
- buildHeadersSummary,
337
- };
26
+ export { extractDeliveryId, buildHeadersSummary };