@wrongstack/core 0.286.0 → 0.287.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot.d.ts.map +1 -1
- package/dist/coordination/brain-monitor.d.ts +15 -0
- package/dist/coordination/brain-monitor.d.ts.map +1 -1
- package/dist/coordination/delegate-tool.d.ts +8 -14
- package/dist/coordination/delegate-tool.d.ts.map +1 -1
- package/dist/coordination/director/director-btw-notes.d.ts +20 -0
- package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
- package/dist/coordination/director/director-collab.d.ts +40 -0
- package/dist/coordination/director/director-collab.d.ts.map +1 -0
- package/dist/coordination/director.d.ts +27 -8
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/fleet-manager.d.ts +21 -0
- package/dist/coordination/fleet-manager.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +2 -3
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +36 -22
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/icoordinator.d.ts +1 -3
- package/dist/coordination/icoordinator.d.ts.map +1 -1
- package/dist/coordination/index.js +708 -442
- package/dist/coordination/index.js.map +4 -4
- package/dist/coordination/mail-tools.d.ts.map +1 -1
- package/dist/coordination/mailbox-message-codec.d.ts +21 -1
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
- package/dist/coordination/mailbox-types.d.ts +42 -1
- package/dist/coordination/mailbox-types.d.ts.map +1 -1
- package/dist/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-response.d.ts +10 -1
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent.d.ts +8 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/context.d.ts +25 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/conversation-state.d.ts +6 -0
- package/dist/core/conversation-state.d.ts.map +1 -1
- package/dist/core/fallback-model.d.ts.map +1 -1
- package/dist/core/mailbox-loop.d.ts +12 -5
- package/dist/core/mailbox-loop.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +14 -6
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/defaults/index.d.ts +0 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +834 -1465
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/auto-compaction-middleware.d.ts +8 -1
- package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
- package/dist/execution/compaction-core.d.ts.map +1 -1
- package/dist/execution/council-brain.d.ts.map +1 -1
- package/dist/execution/council-orchestrator.d.ts +32 -0
- package/dist/execution/council-orchestrator.d.ts.map +1 -0
- package/dist/execution/council-personas.d.ts +19 -0
- package/dist/execution/council-personas.d.ts.map +1 -0
- package/dist/execution/council-profiles.d.ts +34 -0
- package/dist/execution/council-profiles.d.ts.map +1 -0
- package/dist/execution/council-prompts.d.ts +21 -0
- package/dist/execution/council-prompts.d.ts.map +1 -0
- package/dist/execution/council-resolution.d.ts +57 -0
- package/dist/execution/council-resolution.d.ts.map +1 -0
- package/dist/execution/design-kit-loader.d.ts.map +1 -1
- package/dist/execution/enhance-recovery.d.ts +35 -0
- package/dist/execution/enhance-recovery.d.ts.map +1 -0
- package/dist/execution/error-handler.d.ts.map +1 -1
- package/dist/execution/index.d.ts +6 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1738 -161
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/intelligent-compactor.d.ts +18 -7
- package/dist/execution/intelligent-compactor.d.ts.map +1 -1
- package/dist/execution/one-shot-llm.d.ts +52 -0
- package/dist/execution/one-shot-llm.d.ts.map +1 -0
- package/dist/execution/prompt-enhancer.d.ts +23 -4
- package/dist/execution/prompt-enhancer.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.js +24 -6
- package/dist/execution/prompt-enhancer.js.map +2 -2
- package/dist/execution/selective-compactor.d.ts +4 -4
- package/dist/execution/strategy-compactor.d.ts +7 -0
- package/dist/execution/strategy-compactor.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts +19 -5
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/runner.d.ts.map +1 -1
- package/dist/hooks/shell-executor.d.ts.map +1 -1
- package/dist/hq/alerts.d.ts +5 -0
- package/dist/hq/alerts.d.ts.map +1 -1
- package/dist/hq/auth-store.d.ts +17 -7
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/commands.d.ts +8 -4
- package/dist/hq/commands.d.ts.map +1 -1
- package/dist/hq/cost-bridge.d.ts +5 -3
- package/dist/hq/cost-bridge.d.ts.map +1 -1
- package/dist/hq/fleet-bridge.d.ts.map +1 -1
- package/dist/hq/index.d.ts +0 -1
- package/dist/hq/index.d.ts.map +1 -1
- package/dist/hq/index.js +471 -294
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/persistence.d.ts +42 -0
- package/dist/hq/persistence.d.ts.map +1 -1
- package/dist/hq/protocol/brain.d.ts +24 -0
- package/dist/hq/protocol/brain.d.ts.map +1 -0
- package/dist/hq/protocol/browser.d.ts +12 -0
- package/dist/hq/protocol/browser.d.ts.map +1 -0
- package/dist/hq/protocol/client.d.ts +69 -0
- package/dist/hq/protocol/client.d.ts.map +1 -0
- package/dist/hq/protocol/core.d.ts +130 -0
- package/dist/hq/protocol/core.d.ts.map +1 -0
- package/dist/hq/protocol/fleet.d.ts +73 -0
- package/dist/hq/protocol/fleet.d.ts.map +1 -0
- package/dist/hq/protocol/mailbox.d.ts +76 -0
- package/dist/hq/protocol/mailbox.d.ts.map +1 -0
- package/dist/hq/protocol/mcp.d.ts +51 -0
- package/dist/hq/protocol/mcp.d.ts.map +1 -0
- package/dist/hq/protocol/project.d.ts +26 -0
- package/dist/hq/protocol/project.d.ts.map +1 -0
- package/dist/hq/protocol/session.d.ts +153 -0
- package/dist/hq/protocol/session.d.ts.map +1 -0
- package/dist/hq/protocol/tool.d.ts +31 -0
- package/dist/hq/protocol/tool.d.ts.map +1 -0
- package/dist/hq/protocol.d.ts +10 -611
- package/dist/hq/protocol.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4540 -2283
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +13 -11
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/infrastructure/token-counter.d.ts.map +1 -1
- package/dist/kernel/events/agent-events.d.ts +283 -0
- package/dist/kernel/events/agent-events.d.ts.map +1 -0
- package/dist/kernel/events/brain-events.d.ts +176 -0
- package/dist/kernel/events/brain-events.d.ts.map +1 -0
- package/dist/kernel/events/fleet-events.d.ts +39 -0
- package/dist/kernel/events/fleet-events.d.ts.map +1 -0
- package/dist/kernel/events/memory-events.d.ts +165 -0
- package/dist/kernel/events/memory-events.d.ts.map +1 -0
- package/dist/kernel/events/provider-events.d.ts +98 -0
- package/dist/kernel/events/provider-events.d.ts.map +1 -0
- package/dist/kernel/events/sdd-events.d.ts +118 -0
- package/dist/kernel/events/sdd-events.d.ts.map +1 -0
- package/dist/kernel/events/session-events.d.ts +227 -0
- package/dist/kernel/events/session-events.d.ts.map +1 -0
- package/dist/kernel/events/tool-events.d.ts +185 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -0
- package/dist/kernel/events/worktree-events.d.ts +58 -0
- package/dist/kernel/events/worktree-events.d.ts.map +1 -0
- package/dist/kernel/events.d.ts +10 -1298
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js +22 -2
- package/dist/kernel/index.js.map +2 -2
- package/dist/kernel/run-controller.d.ts +15 -0
- package/dist/kernel/run-controller.d.ts.map +1 -1
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/models/index.js +19 -6
- package/dist/models/index.js.map +2 -2
- package/dist/models/llm-selector.d.ts +7 -0
- package/dist/models/llm-selector.d.ts.map +1 -1
- package/dist/plugins/auto-review-plugin.d.ts +36 -0
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
- package/dist/plugins/chimera-plugin.d.ts +8 -0
- package/dist/plugins/chimera-plugin.d.ts.map +1 -1
- package/dist/security/index.js +7 -2
- package/dist/security/index.js.map +2 -2
- package/dist/security/secret-scrubber.d.ts.map +1 -1
- package/dist/skills/index.js.map +2 -2
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/goal-kanban.d.ts +69 -0
- package/dist/storage/goal-kanban.d.ts.map +1 -0
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +425 -717
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/memory-consolidator.d.ts +9 -1
- package/dist/storage/memory-consolidator.d.ts.map +1 -1
- package/dist/tools/council-tool.d.ts +25 -0
- package/dist/tools/council-tool.d.ts.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1884 -12
- package/dist/tools/index.js.map +4 -4
- package/dist/tools/one-shot-llm-tool.d.ts +51 -0
- package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
- package/dist/types/config.d.ts +27 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/context-window.d.ts.map +1 -1
- package/dist/types/council.d.ts +134 -0
- package/dist/types/council.d.ts.map +1 -0
- package/dist/types/default-config.d.ts +3 -3
- package/dist/types/hooks.d.ts +14 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +29 -8
- package/dist/types/index.js.map +3 -3
- package/dist/types/one-shot-llm.d.ts +128 -0
- package/dist/types/one-shot-llm.d.ts.map +1 -0
- package/dist/types/plugin.d.ts +6 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/provider.d.ts +17 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/system-prompt.d.ts.map +1 -1
- package/dist/types/tool-executor.d.ts +10 -0
- package/dist/types/tool-executor.d.ts.map +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/index.js +75 -8
- package/dist/utils/index.js.map +3 -3
- package/dist/utils/tool-wire-compact.d.ts +1 -0
- package/dist/utils/tool-wire-compact.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +20 -4
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- package/instructions/cli/next-task-predictor.md +5 -3
- package/instructions/coordination/subagent-baseline.md +8 -7
- package/instructions/leader-after-task.md +18 -9
- package/instructions/llm/council-judge.md +19 -0
- package/instructions/llm/council-voter.md +22 -0
- package/instructions/modes/architect.md +14 -9
- package/instructions/modes/audit-lite.md +9 -7
- package/instructions/modes/brief-system.md +12 -12
- package/instructions/modes/brief.md +7 -5
- package/instructions/modes/code-auditor.md +13 -8
- package/instructions/modes/code-reviewer.md +13 -8
- package/instructions/modes/debug-lite.md +7 -6
- package/instructions/modes/debugger.md +13 -9
- package/instructions/modes/devops.md +13 -9
- package/instructions/modes/plan-lite.md +7 -6
- package/instructions/modes/refactor-lite.md +7 -6
- package/instructions/modes/refactorer.md +14 -9
- package/instructions/modes/research-lite.md +8 -6
- package/instructions/modes/research-web.md +18 -55
- package/instructions/modes/review-lite.md +7 -6
- package/instructions/modes/teach-system.md +15 -15
- package/instructions/modes/teach.md +13 -12
- package/instructions/modes/test-lite.md +6 -5
- package/instructions/modes/tester.md +13 -9
- package/instructions/modes/ui-design.md +14 -8
- package/instructions/sections/tool/commit-hygiene.md +1 -7
- package/instructions/sections/tool/common-patterns.md +5 -5
- package/instructions/sections/tool/delegation-full.md +3 -31
- package/instructions/sections/tool/mailbox-compact.md +1 -1
- package/instructions/sections/tool/mailbox-full.md +6 -22
- package/instructions/system.md +278 -39
- package/package.json +2 -2
- package/skills/mailbox-bridge/SKILL.md +16 -7
- package/skills/output-standards/SKILL.md +16 -16
- package/skills/wrongstack-mailbox/SKILL.md +6 -0
- package/dist/coordination/director-construction.d.ts +0 -90
- package/dist/coordination/director-construction.d.ts.map +0 -1
- package/dist/hq/agent-bridge.d.ts +0 -27
- package/dist/hq/agent-bridge.d.ts.map +0 -1
- package/dist/storage/memory-store.d.ts +0 -119
- package/dist/storage/memory-store.d.ts.map +0 -1
package/dist/hq/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// src/hq/protocol.ts
|
|
2
|
-
var HQ_PROTOCOL_VERSION = 1;
|
|
1
|
+
// src/hq/protocol/tool.ts
|
|
3
2
|
var DEFAULT_HQ_REDACTION_POLICY = {
|
|
4
3
|
rawContent: true,
|
|
5
4
|
toolArgs: "full",
|
|
6
5
|
paths: "full"
|
|
7
6
|
};
|
|
8
7
|
var HQ_TRANSCRIPT_TEXT_CAP = 16e3;
|
|
8
|
+
|
|
9
|
+
// src/hq/protocol/core.ts
|
|
10
|
+
var HQ_PROTOCOL_VERSION = 1;
|
|
9
11
|
var KNOWN_HQ_CLIENT_FRAME_TYPES = /* @__PURE__ */ new Set([
|
|
10
12
|
"client.hello",
|
|
11
13
|
"client.event",
|
|
@@ -543,7 +545,12 @@ function hasCredentialAnchors(text) {
|
|
|
543
545
|
text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
|
|
544
546
|
text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
|
|
545
547
|
text.includes("_PASSWORD=") || // DB_PASSWORD=, ROOT_PASSWORD=, ...
|
|
546
|
-
text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://")
|
|
548
|
+
text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://") || // JSON-style credential keys: tool outputs often serialise objects as
|
|
549
|
+
// raw JSON strings rather than parsed objects. The value may not start
|
|
550
|
+
// with a recognisable prefix (sk-, ghp_, etc.), so we anchor on common
|
|
551
|
+
// key names. The `"` prefix avoids matching prose that happens to mention
|
|
552
|
+
// these words — JSON serialisation always quotes string keys.
|
|
553
|
+
text.includes('"apiKey"') || text.includes('"api_key"') || text.includes('"token"') || text.includes('"secret"') || text.includes('"password"') || text.includes('"authorization"') || text.includes('"bearer"') || text.includes('"private_key"') || text.includes('"access_token"') || text.includes('"refresh_token"') || text.includes('"client_secret"');
|
|
547
554
|
}
|
|
548
555
|
var DefaultSecretScrubber = class {
|
|
549
556
|
scrub(text) {
|
|
@@ -1381,7 +1388,7 @@ var HqPublisher = class {
|
|
|
1381
1388
|
|
|
1382
1389
|
// src/hq/factory.ts
|
|
1383
1390
|
import { createHash as createHash2, randomUUID as randomUUID4 } from "node:crypto";
|
|
1384
|
-
import * as
|
|
1391
|
+
import * as fs4 from "node:fs";
|
|
1385
1392
|
import { hostname } from "node:os";
|
|
1386
1393
|
import { basename as basename5 } from "node:path";
|
|
1387
1394
|
|
|
@@ -1392,7 +1399,8 @@ import * as path4 from "node:path";
|
|
|
1392
1399
|
|
|
1393
1400
|
// src/utils/atomic-write.ts
|
|
1394
1401
|
import { randomBytes } from "node:crypto";
|
|
1395
|
-
import * as
|
|
1402
|
+
import * as fs2 from "node:fs/promises";
|
|
1403
|
+
import { watch as watchDir } from "node:fs";
|
|
1396
1404
|
import * as path3 from "node:path";
|
|
1397
1405
|
|
|
1398
1406
|
// src/utils/session-scoped-path.ts
|
|
@@ -1419,14 +1427,34 @@ function invalid(sessionId) {
|
|
|
1419
1427
|
|
|
1420
1428
|
// src/utils/wstack-paths.ts
|
|
1421
1429
|
import { createHash } from "node:crypto";
|
|
1430
|
+
import * as fs from "node:fs";
|
|
1422
1431
|
import * as os from "node:os";
|
|
1423
1432
|
import * as path2 from "node:path";
|
|
1433
|
+
function canonicalProjectRoot(absRoot) {
|
|
1434
|
+
const checkoutRoot = path2.resolve(absRoot);
|
|
1435
|
+
const dotGit = path2.join(checkoutRoot, ".git");
|
|
1436
|
+
try {
|
|
1437
|
+
if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
|
|
1438
|
+
const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
|
|
1439
|
+
const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
|
|
1440
|
+
if (!match?.[1]) return checkoutRoot;
|
|
1441
|
+
const gitDir = path2.resolve(checkoutRoot, match[1].trim());
|
|
1442
|
+
const commonDirFile = path2.join(gitDir, "commondir");
|
|
1443
|
+
if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
|
|
1444
|
+
const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
|
|
1445
|
+
if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
|
|
1446
|
+
return path2.dirname(commonDir);
|
|
1447
|
+
} catch {
|
|
1448
|
+
return checkoutRoot;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1424
1451
|
function projectHash(absRoot) {
|
|
1425
|
-
return createHash("sha256").update(
|
|
1452
|
+
return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
|
|
1426
1453
|
}
|
|
1427
1454
|
function projectSlug(absRoot) {
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1455
|
+
const identityRoot = canonicalProjectRoot(absRoot);
|
|
1456
|
+
const base = slugify(path2.basename(identityRoot));
|
|
1457
|
+
const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
|
|
1430
1458
|
return `${base}-${hash}`;
|
|
1431
1459
|
}
|
|
1432
1460
|
function slugify(name) {
|
|
@@ -1597,16 +1625,16 @@ var FsError = class extends WrongStackError {
|
|
|
1597
1625
|
// src/utils/atomic-write.ts
|
|
1598
1626
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
1599
1627
|
const dir = path3.dirname(targetPath);
|
|
1600
|
-
await
|
|
1628
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1601
1629
|
const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
1602
1630
|
try {
|
|
1603
1631
|
if (typeof content === "string") {
|
|
1604
|
-
await
|
|
1632
|
+
await fs2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
1605
1633
|
} else {
|
|
1606
|
-
await
|
|
1634
|
+
await fs2.writeFile(tmp, content, { flag: "wx" });
|
|
1607
1635
|
}
|
|
1608
1636
|
try {
|
|
1609
|
-
const fh = await
|
|
1637
|
+
const fh = await fs2.open(tmp, "r+");
|
|
1610
1638
|
try {
|
|
1611
1639
|
await fh.sync();
|
|
1612
1640
|
} finally {
|
|
@@ -1616,24 +1644,24 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
1616
1644
|
}
|
|
1617
1645
|
let mode;
|
|
1618
1646
|
try {
|
|
1619
|
-
const stat4 = await
|
|
1647
|
+
const stat4 = await fs2.stat(targetPath);
|
|
1620
1648
|
mode = stat4.mode & 511;
|
|
1621
1649
|
} catch {
|
|
1622
1650
|
mode = opts.mode;
|
|
1623
1651
|
}
|
|
1624
1652
|
if (mode !== void 0) {
|
|
1625
|
-
await
|
|
1653
|
+
await fs2.chmod(tmp, mode);
|
|
1626
1654
|
}
|
|
1627
1655
|
await renameWithRetry(tmp, targetPath);
|
|
1628
1656
|
if (mode !== void 0 && process.platform === "win32") {
|
|
1629
1657
|
try {
|
|
1630
|
-
await
|
|
1658
|
+
await fs2.chmod(targetPath, mode);
|
|
1631
1659
|
} catch {
|
|
1632
1660
|
}
|
|
1633
1661
|
}
|
|
1634
1662
|
} catch (err) {
|
|
1635
1663
|
try {
|
|
1636
|
-
await
|
|
1664
|
+
await fs2.unlink(tmp);
|
|
1637
1665
|
} catch {
|
|
1638
1666
|
}
|
|
1639
1667
|
throw err;
|
|
@@ -1641,7 +1669,7 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
1641
1669
|
}
|
|
1642
1670
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
1643
1671
|
const dir = path3.dirname(targetPath);
|
|
1644
|
-
await
|
|
1672
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1645
1673
|
const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
|
|
1646
1674
|
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
1647
1675
|
const staleMs = opts.staleMs ?? 3e4;
|
|
@@ -1649,26 +1677,27 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
1649
1677
|
let handle;
|
|
1650
1678
|
for (; ; ) {
|
|
1651
1679
|
try {
|
|
1652
|
-
handle = await
|
|
1680
|
+
handle = await fs2.open(lockPath, "wx");
|
|
1653
1681
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
1654
1682
|
break;
|
|
1655
1683
|
} catch (err) {
|
|
1656
1684
|
const code = err.code;
|
|
1657
1685
|
if (code === "ENOENT") {
|
|
1658
|
-
await
|
|
1686
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1659
1687
|
continue;
|
|
1660
1688
|
}
|
|
1661
|
-
if (code !== "EEXIST") throw err;
|
|
1689
|
+
if (code !== "EEXIST" && code !== "EPERM") throw err;
|
|
1662
1690
|
try {
|
|
1663
|
-
const stat4 = await
|
|
1691
|
+
const stat4 = await fs2.stat(lockPath);
|
|
1664
1692
|
if (Date.now() - stat4.mtimeMs > staleMs) {
|
|
1665
|
-
await
|
|
1693
|
+
await fs2.unlink(lockPath);
|
|
1666
1694
|
continue;
|
|
1667
1695
|
}
|
|
1668
1696
|
} catch {
|
|
1669
1697
|
continue;
|
|
1670
1698
|
}
|
|
1671
|
-
|
|
1699
|
+
const elapsed = Date.now() - started;
|
|
1700
|
+
if (elapsed >= timeoutMs) {
|
|
1672
1701
|
throw new FsError({
|
|
1673
1702
|
message: `Timed out waiting for file lock: ${targetPath}`,
|
|
1674
1703
|
code: "FS_ATOMIC_WRITE_FAILED",
|
|
@@ -1676,7 +1705,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
1676
1705
|
context: { timeoutMs }
|
|
1677
1706
|
});
|
|
1678
1707
|
}
|
|
1679
|
-
await
|
|
1708
|
+
await waitForLockRelease(lockPath, timeoutMs - elapsed);
|
|
1680
1709
|
}
|
|
1681
1710
|
}
|
|
1682
1711
|
try {
|
|
@@ -1687,22 +1716,66 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
1687
1716
|
} catch {
|
|
1688
1717
|
}
|
|
1689
1718
|
try {
|
|
1690
|
-
await
|
|
1719
|
+
await fs2.unlink(lockPath);
|
|
1691
1720
|
} catch {
|
|
1692
1721
|
}
|
|
1693
1722
|
}
|
|
1694
1723
|
}
|
|
1724
|
+
async function waitForLockRelease(lockPath, remainingMs) {
|
|
1725
|
+
const parentDir = path3.dirname(lockPath);
|
|
1726
|
+
const lockName = path3.basename(lockPath);
|
|
1727
|
+
const intervalMs = Math.min(remainingMs, 100);
|
|
1728
|
+
return new Promise((resolve4) => {
|
|
1729
|
+
let settled = false;
|
|
1730
|
+
let watcher = null;
|
|
1731
|
+
const timer = setTimeout(() => {
|
|
1732
|
+
settled = true;
|
|
1733
|
+
watcher?.close();
|
|
1734
|
+
resolve4();
|
|
1735
|
+
}, intervalMs);
|
|
1736
|
+
try {
|
|
1737
|
+
watcher = watchDir(parentDir, (eventType, filename) => {
|
|
1738
|
+
if (settled) return;
|
|
1739
|
+
if (filename === lockName && (eventType === "rename" || eventType === "change")) {
|
|
1740
|
+
settled = true;
|
|
1741
|
+
clearTimeout(timer);
|
|
1742
|
+
watcher?.close();
|
|
1743
|
+
resolve4();
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
} catch {
|
|
1747
|
+
clearTimeout(timer);
|
|
1748
|
+
if (!settled) {
|
|
1749
|
+
settled = true;
|
|
1750
|
+
setTimeout(resolve4, Math.min(remainingMs, 25));
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
fs2.access(lockPath).then(
|
|
1755
|
+
() => {
|
|
1756
|
+
},
|
|
1757
|
+
() => {
|
|
1758
|
+
if (!settled) {
|
|
1759
|
+
settled = true;
|
|
1760
|
+
clearTimeout(timer);
|
|
1761
|
+
watcher?.close();
|
|
1762
|
+
resolve4();
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
);
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1695
1768
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
1696
1769
|
async function renameWithRetry(from, to) {
|
|
1697
1770
|
if (process.platform !== "win32") {
|
|
1698
|
-
await
|
|
1771
|
+
await fs2.rename(from, to);
|
|
1699
1772
|
return;
|
|
1700
1773
|
}
|
|
1701
1774
|
const delays = [10, 25, 60, 120, 250];
|
|
1702
1775
|
let lastErr;
|
|
1703
1776
|
for (let i = 0; i <= delays.length; i++) {
|
|
1704
1777
|
try {
|
|
1705
|
-
await
|
|
1778
|
+
await fs2.rename(from, to);
|
|
1706
1779
|
return;
|
|
1707
1780
|
} catch (err) {
|
|
1708
1781
|
lastErr = err;
|
|
@@ -1854,8 +1927,41 @@ function parseMailboxMessage(value) {
|
|
|
1854
1927
|
...taskContext === void 0 ? {} : { taskContext }
|
|
1855
1928
|
};
|
|
1856
1929
|
}
|
|
1857
|
-
function
|
|
1858
|
-
return
|
|
1930
|
+
function isAckRecord(value) {
|
|
1931
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && value["__ack"] === true;
|
|
1932
|
+
}
|
|
1933
|
+
function parseMailboxLine(line) {
|
|
1934
|
+
try {
|
|
1935
|
+
const parsed = JSON.parse(line);
|
|
1936
|
+
if (isAckRecord(parsed)) return parsed;
|
|
1937
|
+
return parseMailboxMessage(parsed);
|
|
1938
|
+
} catch {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
function applyAckToMessage(msg, ack) {
|
|
1943
|
+
if (ack.read && !(ack.readerId in msg.readBy)) {
|
|
1944
|
+
msg.readBy[ack.readerId] = ack.timestamp;
|
|
1945
|
+
}
|
|
1946
|
+
if (ack.completed && !msg.completed) {
|
|
1947
|
+
msg.completed = true;
|
|
1948
|
+
msg.completedBy = ack.completedBy ?? ack.readerId;
|
|
1949
|
+
msg.completedAt = ack.timestamp;
|
|
1950
|
+
}
|
|
1951
|
+
if (ack.outcome !== void 0 && msg.outcome !== ack.outcome) {
|
|
1952
|
+
msg.outcome = ack.outcome;
|
|
1953
|
+
}
|
|
1954
|
+
if (ack.deleted === true) {
|
|
1955
|
+
msg.deletedAt = ack.timestamp;
|
|
1956
|
+
msg.deletedBy = ack.deletedBy ?? ack.readerId;
|
|
1957
|
+
}
|
|
1958
|
+
if (ack.deleted === false) {
|
|
1959
|
+
delete msg.deletedAt;
|
|
1960
|
+
delete msg.deletedBy;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function serializeAckRecord(ack) {
|
|
1964
|
+
return JSON.stringify(ack) + "\n";
|
|
1859
1965
|
}
|
|
1860
1966
|
|
|
1861
1967
|
// src/coordination/mailbox-types.ts
|
|
@@ -1925,7 +2031,7 @@ var GlobalMailbox = class {
|
|
|
1925
2031
|
* appended message). The chain runs each read to completion before
|
|
1926
2032
|
* the next starts, in issue order — readers are read-only and never
|
|
1927
2033
|
* conflict with each other on content, only on the cache mutation
|
|
1928
|
-
* that follows the read. Pattern mirrors
|
|
2034
|
+
* that follows the read. Pattern mirrors a serialized mutation chain.
|
|
1929
2035
|
*/
|
|
1930
2036
|
_readChain = Promise.resolve([]);
|
|
1931
2037
|
/**
|
|
@@ -1936,7 +2042,7 @@ var GlobalMailbox = class {
|
|
|
1936
2042
|
*
|
|
1937
2043
|
* The index is rebuilt whenever the cache array is replaced
|
|
1938
2044
|
* (`_setMessageCache`), extended when new messages are pushed
|
|
1939
|
-
* (`_pushToCache
|
|
2045
|
+
* (`_pushToCache`), and cleared on `close()`.
|
|
1940
2046
|
* It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
|
|
1941
2047
|
* or never populated).
|
|
1942
2048
|
*
|
|
@@ -2075,42 +2181,100 @@ var GlobalMailbox = class {
|
|
|
2075
2181
|
const updated = await this.ackMany({ acks: [input] });
|
|
2076
2182
|
return updated.length > 0 ? updated[0] : null;
|
|
2077
2183
|
}
|
|
2184
|
+
/** Diagnostic counters exposed for test/tool introspection. */
|
|
2185
|
+
diag = {
|
|
2186
|
+
ackManyCacheDesync: 0,
|
|
2187
|
+
ackManyPreLockMtime: 0,
|
|
2188
|
+
ackManyPostLockMtime: 0,
|
|
2189
|
+
ackManyPreLockSize: 0,
|
|
2190
|
+
ackManyPostLockSize: 0,
|
|
2191
|
+
ackManyMessageCount: 0,
|
|
2192
|
+
ackManyAckCount: 0,
|
|
2193
|
+
cacheHitCount: 0,
|
|
2194
|
+
cacheMissCount: 0,
|
|
2195
|
+
cacheDesyncNoOp: 0,
|
|
2196
|
+
applyAckTargetMissing: 0,
|
|
2197
|
+
setMessageCacheCount: 0,
|
|
2198
|
+
pushToCacheCount: 0
|
|
2199
|
+
};
|
|
2078
2200
|
async ackMany(input) {
|
|
2079
2201
|
if (input.acks.length === 0) return [];
|
|
2202
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2203
|
+
const ackRecords = [];
|
|
2080
2204
|
const updated = [];
|
|
2205
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
2081
2206
|
const byId = /* @__PURE__ */ new Map();
|
|
2082
2207
|
for (const a of input.acks) {
|
|
2083
2208
|
byId.set(a.messageId, a);
|
|
2209
|
+
targetIds.add(a.messageId);
|
|
2084
2210
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
msg.completedBy = a.readerId;
|
|
2100
|
-
msg.completedAt = now;
|
|
2101
|
-
changed = true;
|
|
2102
|
-
}
|
|
2103
|
-
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
2104
|
-
msg.outcome = a.outcome;
|
|
2105
|
-
changed = true;
|
|
2106
|
-
}
|
|
2211
|
+
const preLockMtime = this._messageCacheMtime;
|
|
2212
|
+
const preLockSize = this._messageCacheSize;
|
|
2213
|
+
this.diag.ackManyPreLockMtime = preLockMtime;
|
|
2214
|
+
this.diag.ackManyPreLockSize = preLockSize;
|
|
2215
|
+
const all = await this._readMessagesCached();
|
|
2216
|
+
for (const msg of all) {
|
|
2217
|
+
const a = byId.get(msg.id);
|
|
2218
|
+
if (!a) continue;
|
|
2219
|
+
const needsRead = a.read !== false && !(a.readerId in msg.readBy);
|
|
2220
|
+
const needsComplete = a.completed && !msg.completed;
|
|
2221
|
+
const needsOutcome = a.outcome !== void 0 && msg.outcome !== a.outcome;
|
|
2222
|
+
if (!needsRead && !needsComplete && !needsOutcome) {
|
|
2223
|
+
updated.push({ ...msg, readBy: { ...msg.readBy } });
|
|
2224
|
+
continue;
|
|
2107
2225
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2226
|
+
const copy = { ...msg, readBy: { ...msg.readBy } };
|
|
2227
|
+
if (needsRead) copy.readBy[a.readerId] = now;
|
|
2228
|
+
if (needsComplete) {
|
|
2229
|
+
copy.completed = true;
|
|
2230
|
+
copy.completedBy = a.readerId;
|
|
2231
|
+
copy.completedAt = now;
|
|
2111
2232
|
}
|
|
2233
|
+
if (needsOutcome) copy.outcome = a.outcome;
|
|
2234
|
+
updated.push(copy);
|
|
2235
|
+
ackRecords.push({
|
|
2236
|
+
__ack: true,
|
|
2237
|
+
messageId: msg.id,
|
|
2238
|
+
readerId: a.readerId,
|
|
2239
|
+
timestamp: now,
|
|
2240
|
+
read: a.read !== false,
|
|
2241
|
+
completed: a.completed,
|
|
2242
|
+
completedBy: a.completed ? a.readerId : void 0,
|
|
2243
|
+
outcome: a.outcome
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
if (ackRecords.length === 0) {
|
|
2247
|
+
if (updated.length > 0) {
|
|
2248
|
+
for (const message of updated) {
|
|
2249
|
+
this.publishHqMailboxEvent({
|
|
2250
|
+
mailboxId: this.hqMailboxId,
|
|
2251
|
+
action: message.completed ? "message.completed" : "message.read",
|
|
2252
|
+
message
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return updated;
|
|
2257
|
+
}
|
|
2258
|
+
const serialized = ackRecords.map((a) => serializeAckRecord(a)).join("");
|
|
2259
|
+
await withFileLock(this.messagePath, async () => {
|
|
2260
|
+
await fsp.appendFile(this.messagePath, serialized, "utf8");
|
|
2112
2261
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
2113
|
-
|
|
2262
|
+
for (const ack of ackRecords) {
|
|
2263
|
+
this._applyAckToCache(ack);
|
|
2264
|
+
}
|
|
2265
|
+
this._messageCacheMtime = mtimeMs;
|
|
2266
|
+
this._messageCacheSize = size;
|
|
2267
|
+
this.diag.ackManyPostLockMtime = mtimeMs;
|
|
2268
|
+
this.diag.ackManyPostLockSize = size;
|
|
2269
|
+
this.diag.ackManyMessageCount = this._messageCache?.length ?? -1;
|
|
2270
|
+
this.diag.ackManyAckCount = ackRecords.length;
|
|
2271
|
+
const expectedMinSize = preLockSize + serialized.length;
|
|
2272
|
+
if (preLockSize > 0 && size > expectedMinSize) {
|
|
2273
|
+
const extraBytes = size - expectedMinSize;
|
|
2274
|
+
console.error(
|
|
2275
|
+
`[MAILBOX-DIAG] ackMany: post-lock size ${size} exceeds expected ${expectedMinSize} by ${extraBytes}B (mtime ${preLockMtime}->${mtimeMs}). Cache may be missing cross-process messages. diag.ackManyCacheDesync=${++this.diag.ackManyCacheDesync}`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2114
2278
|
});
|
|
2115
2279
|
for (const message of updated) {
|
|
2116
2280
|
this.publishHqMailboxEvent({
|
|
@@ -2153,87 +2317,87 @@ var GlobalMailbox = class {
|
|
|
2153
2317
|
return count;
|
|
2154
2318
|
}
|
|
2155
2319
|
async softDelete(mailId, by) {
|
|
2156
|
-
|
|
2320
|
+
const all = await this._readMessagesCached();
|
|
2321
|
+
const target = all.find((m) => m.id === mailId);
|
|
2322
|
+
if (target === void 0) return null;
|
|
2323
|
+
if (target.deletedAt !== void 0) {
|
|
2324
|
+
return { ...target, readBy: { ...target.readBy } };
|
|
2325
|
+
}
|
|
2326
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2327
|
+
const ack = {
|
|
2328
|
+
__ack: true,
|
|
2329
|
+
messageId: mailId,
|
|
2330
|
+
readerId: by,
|
|
2331
|
+
timestamp: now,
|
|
2332
|
+
read: true,
|
|
2333
|
+
deleted: true,
|
|
2334
|
+
deletedBy: by
|
|
2335
|
+
};
|
|
2157
2336
|
await withFileLock(this.messagePath, async () => {
|
|
2158
|
-
|
|
2159
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2160
|
-
let changed = false;
|
|
2161
|
-
for (const m of all) {
|
|
2162
|
-
if (m.id !== mailId) continue;
|
|
2163
|
-
if (m.deletedAt !== void 0) {
|
|
2164
|
-
updated = m;
|
|
2165
|
-
continue;
|
|
2166
|
-
}
|
|
2167
|
-
m.deletedAt = now;
|
|
2168
|
-
m.deletedBy = by;
|
|
2169
|
-
updated = m;
|
|
2170
|
-
changed = true;
|
|
2171
|
-
}
|
|
2172
|
-
if (changed) {
|
|
2173
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
2174
|
-
await fsp.writeFile(this.messagePath, serialized, "utf8");
|
|
2175
|
-
}
|
|
2337
|
+
await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
|
|
2176
2338
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
2177
|
-
this.
|
|
2339
|
+
this._applyAckToCache(ack);
|
|
2340
|
+
this._messageCacheMtime = mtimeMs;
|
|
2341
|
+
this._messageCacheSize = size;
|
|
2178
2342
|
});
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
2195
|
-
return
|
|
2343
|
+
const msg = { ...target, readBy: { ...target.readBy } };
|
|
2344
|
+
msg.deletedAt = now;
|
|
2345
|
+
msg.deletedBy = by;
|
|
2346
|
+
this.publishHqMailboxEvent({
|
|
2347
|
+
mailboxId: this.hqMailboxId,
|
|
2348
|
+
action: "message.updated",
|
|
2349
|
+
message: msg
|
|
2350
|
+
});
|
|
2351
|
+
this.publishHqMailboxSnapshot();
|
|
2352
|
+
this.eventEmitter?.emit({
|
|
2353
|
+
type: "message.deleted",
|
|
2354
|
+
messageId: msg.id,
|
|
2355
|
+
from: msg.from,
|
|
2356
|
+
to: msg.to,
|
|
2357
|
+
timestamp: msg.deletedAt ?? now
|
|
2358
|
+
});
|
|
2359
|
+
return msg;
|
|
2196
2360
|
}
|
|
2197
2361
|
async restore(mailId) {
|
|
2198
|
-
|
|
2362
|
+
const all = await this._readMessagesCached();
|
|
2363
|
+
const target = all.find((m) => m.id === mailId);
|
|
2364
|
+
if (target === void 0) return null;
|
|
2365
|
+
if (target.deletedAt === void 0 && target.deletedBy === void 0) {
|
|
2366
|
+
return { ...target, readBy: { ...target.readBy } };
|
|
2367
|
+
}
|
|
2368
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2369
|
+
const ack = {
|
|
2370
|
+
__ack: true,
|
|
2371
|
+
messageId: mailId,
|
|
2372
|
+
readerId: "",
|
|
2373
|
+
timestamp: now,
|
|
2374
|
+
read: true,
|
|
2375
|
+
deleted: false
|
|
2376
|
+
};
|
|
2199
2377
|
await withFileLock(this.messagePath, async () => {
|
|
2200
|
-
|
|
2201
|
-
let changed = false;
|
|
2202
|
-
for (const m of all) {
|
|
2203
|
-
if (m.id !== mailId) continue;
|
|
2204
|
-
if (m.deletedAt === void 0 && m.deletedBy === void 0) {
|
|
2205
|
-
updated = m;
|
|
2206
|
-
continue;
|
|
2207
|
-
}
|
|
2208
|
-
delete m.deletedAt;
|
|
2209
|
-
delete m.deletedBy;
|
|
2210
|
-
updated = m;
|
|
2211
|
-
changed = true;
|
|
2212
|
-
}
|
|
2213
|
-
if (changed) {
|
|
2214
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
2215
|
-
await fsp.writeFile(this.messagePath, serialized, "utf8");
|
|
2216
|
-
}
|
|
2378
|
+
await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
|
|
2217
2379
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
2218
|
-
this.
|
|
2380
|
+
this._applyAckToCache(ack);
|
|
2381
|
+
this._messageCacheMtime = mtimeMs;
|
|
2382
|
+
this._messageCacheSize = size;
|
|
2219
2383
|
});
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
}
|
|
2236
|
-
return
|
|
2384
|
+
const msg = { ...target, readBy: { ...target.readBy } };
|
|
2385
|
+
delete msg.deletedAt;
|
|
2386
|
+
delete msg.deletedBy;
|
|
2387
|
+
this.publishHqMailboxEvent({
|
|
2388
|
+
mailboxId: this.hqMailboxId,
|
|
2389
|
+
action: "message.updated",
|
|
2390
|
+
message: msg
|
|
2391
|
+
});
|
|
2392
|
+
this.publishHqMailboxSnapshot();
|
|
2393
|
+
this.eventEmitter?.emit({
|
|
2394
|
+
type: "message.restored",
|
|
2395
|
+
messageId: msg.id,
|
|
2396
|
+
from: msg.from,
|
|
2397
|
+
to: msg.to,
|
|
2398
|
+
timestamp: now
|
|
2399
|
+
});
|
|
2400
|
+
return msg;
|
|
2237
2401
|
}
|
|
2238
2402
|
// ── Agent registry ──────────────────────────────────────────────────────
|
|
2239
2403
|
async registerAgent(input) {
|
|
@@ -2470,11 +2634,18 @@ var GlobalMailbox = class {
|
|
|
2470
2634
|
}
|
|
2471
2635
|
async getClientStatuses() {
|
|
2472
2636
|
await this._ensureClientRegistry();
|
|
2473
|
-
|
|
2637
|
+
let registry = await this._readClientRegistry();
|
|
2474
2638
|
const before = registry.size;
|
|
2475
2639
|
this._pruneStaleClientsInPlace(registry);
|
|
2476
2640
|
if (registry.size < before) {
|
|
2477
|
-
await this.
|
|
2641
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
2642
|
+
registry = await this._readClientRegistry({ fresh: true });
|
|
2643
|
+
const postLockBefore = registry.size;
|
|
2644
|
+
this._pruneStaleClientsInPlace(registry);
|
|
2645
|
+
if (registry.size < postLockBefore) {
|
|
2646
|
+
await this._writeClientRegistry(registry);
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2478
2649
|
}
|
|
2479
2650
|
const now = Date.now();
|
|
2480
2651
|
return Array.from(registry.values()).map((c) => ({
|
|
@@ -2680,75 +2851,22 @@ var GlobalMailbox = class {
|
|
|
2680
2851
|
throw err;
|
|
2681
2852
|
}
|
|
2682
2853
|
}
|
|
2683
|
-
/**
|
|
2684
|
-
* Read only newly-appended bytes from the file and append them to the
|
|
2685
|
-
* in-memory cache. This avoids re-reading and re-parsing the entire file
|
|
2686
|
-
* when another process appended messages since our last read.
|
|
2687
|
-
*
|
|
2688
|
-
* Only safe when the file grew in size (messages are append-only). When
|
|
2689
|
-
* the file was rewritten (ack/purge changed existing content), callers
|
|
2690
|
-
* must fall back to {@link _readMessages}.
|
|
2691
|
-
*
|
|
2692
|
-
* @returns The (now up-to-date) message cache.
|
|
2693
|
-
*/
|
|
2694
|
-
async _readNewMessagesOnly(fd, oldSize, newSize) {
|
|
2695
|
-
const tailLen = newSize - oldSize;
|
|
2696
|
-
const buf = Buffer.alloc(tailLen);
|
|
2697
|
-
await fd.read(buf, 0, tailLen, oldSize);
|
|
2698
|
-
const tail = buf.toString("utf8");
|
|
2699
|
-
if (!tail.endsWith(LINE_SEPARATOR)) {
|
|
2700
|
-
const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR);
|
|
2701
|
-
if (lastSeparatorIdx === -1) {
|
|
2702
|
-
return this._messageCache;
|
|
2703
|
-
}
|
|
2704
|
-
const completeTail = tail.slice(0, lastSeparatorIdx + 1);
|
|
2705
|
-
this._appendIncrementalLines(completeTail);
|
|
2706
|
-
this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
|
|
2707
|
-
return this._messageCache;
|
|
2708
|
-
}
|
|
2709
|
-
this._appendIncrementalLines(tail);
|
|
2710
|
-
this._messageCacheSize = newSize;
|
|
2711
|
-
return this._messageCache;
|
|
2712
|
-
}
|
|
2713
|
-
_appendIncrementalLines(raw) {
|
|
2714
|
-
for (const line of raw.split(LINE_SEPARATOR)) {
|
|
2715
|
-
if (!line.trim()) continue;
|
|
2716
|
-
const message = this._parseLine(line);
|
|
2717
|
-
if (message === null) continue;
|
|
2718
|
-
this._messageCache.push(message);
|
|
2719
|
-
const idx = this._messageCache.length - 1;
|
|
2720
|
-
if (this._recipientIndex !== null) {
|
|
2721
|
-
let set = this._recipientIndex.get(message.to);
|
|
2722
|
-
if (set === void 0) {
|
|
2723
|
-
set = /* @__PURE__ */ new Set();
|
|
2724
|
-
this._recipientIndex.set(message.to, set);
|
|
2725
|
-
}
|
|
2726
|
-
set.add(idx);
|
|
2727
|
-
}
|
|
2728
|
-
if (this._senderIndex !== null) {
|
|
2729
|
-
let set = this._senderIndex.get(message.from);
|
|
2730
|
-
if (set === void 0) {
|
|
2731
|
-
set = /* @__PURE__ */ new Set();
|
|
2732
|
-
this._senderIndex.set(message.from, set);
|
|
2733
|
-
}
|
|
2734
|
-
set.add(idx);
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
_parseLine(line) {
|
|
2739
|
-
try {
|
|
2740
|
-
return parseMailboxMessageLine(line);
|
|
2741
|
-
} catch {
|
|
2742
|
-
return null;
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
/** Parse a JSONL string into MailboxMessage[], including migration. */
|
|
2854
|
+
/** Parse a JSONL string into MailboxMessage[], applying ack records. */
|
|
2746
2855
|
_parseLines(raw) {
|
|
2747
2856
|
const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
2748
2857
|
const messages = [];
|
|
2858
|
+
const acks = [];
|
|
2749
2859
|
for (const line of lines) {
|
|
2750
|
-
const
|
|
2751
|
-
if (
|
|
2860
|
+
const parsed = parseMailboxLine(line);
|
|
2861
|
+
if (isAckRecord(parsed)) {
|
|
2862
|
+
acks.push(parsed);
|
|
2863
|
+
} else if (parsed !== null) {
|
|
2864
|
+
messages.push(parsed);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
for (const ack of acks) {
|
|
2868
|
+
const target = messages.find((m) => m.id === ack.messageId);
|
|
2869
|
+
if (target) applyAckToMessage(target, ack);
|
|
2752
2870
|
}
|
|
2753
2871
|
return messages;
|
|
2754
2872
|
}
|
|
@@ -2831,16 +2949,6 @@ var GlobalMailbox = class {
|
|
|
2831
2949
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
2832
2950
|
return this._messageCache;
|
|
2833
2951
|
}
|
|
2834
|
-
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
2835
|
-
const fd = await fsp.open(this.messagePath, "r");
|
|
2836
|
-
try {
|
|
2837
|
-
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
2838
|
-
this._messageCacheMtime = st.mtimeMs;
|
|
2839
|
-
return updated;
|
|
2840
|
-
} finally {
|
|
2841
|
-
await fd.close();
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
2952
|
const all = await this._readMessages();
|
|
2845
2953
|
this._setMessageCache(all, st.mtimeMs, st.size);
|
|
2846
2954
|
return all;
|
|
@@ -2881,6 +2989,21 @@ var GlobalMailbox = class {
|
|
|
2881
2989
|
this._messageCacheSize = size;
|
|
2882
2990
|
this._rebuildRecipientIndex();
|
|
2883
2991
|
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Fold a single ack record into the in-memory cache, mutating the target
|
|
2994
|
+
* message in place. Called from `ackMany` after appending the record to
|
|
2995
|
+
* disk so the cache reflects the ack without a full re-read + re-parse.
|
|
2996
|
+
*
|
|
2997
|
+
* An ack only touches `readBy`/`completed*`/`outcome` — none of which
|
|
2998
|
+
* affect the recipient/sender indexes — so no index rebuild is needed.
|
|
2999
|
+
* No-op when the cache is empty or the target message isn't cached.
|
|
3000
|
+
*/
|
|
3001
|
+
_applyAckToCache(ack) {
|
|
3002
|
+
const cache = this._messageCache;
|
|
3003
|
+
if (cache === null) return;
|
|
3004
|
+
const target = cache.find((m) => m.id === ack.messageId);
|
|
3005
|
+
if (target !== void 0) applyAckToMessage(target, ack);
|
|
3006
|
+
}
|
|
2884
3007
|
/**
|
|
2885
3008
|
* Build the recipient AND sender indexes from the current `_messageCache`.
|
|
2886
3009
|
* Called only from `_setMessageCache` so both indexes are always in sync
|
|
@@ -3053,7 +3176,7 @@ var GlobalMailbox = class {
|
|
|
3053
3176
|
|
|
3054
3177
|
// src/hq/auth-store.ts
|
|
3055
3178
|
import { randomBytes as randomBytes2, randomUUID as randomUUID3, scrypt, timingSafeEqual } from "node:crypto";
|
|
3056
|
-
import * as
|
|
3179
|
+
import * as fs3 from "node:fs/promises";
|
|
3057
3180
|
import * as syncFs from "node:fs";
|
|
3058
3181
|
import * as path5 from "node:path";
|
|
3059
3182
|
var HQ_AUTH_FILE_VERSION = 1;
|
|
@@ -3113,29 +3236,31 @@ function readHqRuntimeFileSync(dataDir) {
|
|
|
3113
3236
|
return void 0;
|
|
3114
3237
|
}
|
|
3115
3238
|
}
|
|
3116
|
-
async function readHqAuthFile(dataDir,
|
|
3239
|
+
async function readHqAuthFile(dataDir, _opts = {}) {
|
|
3117
3240
|
const file = hqAuthFilePath(dataDir);
|
|
3118
3241
|
let raw;
|
|
3119
3242
|
try {
|
|
3120
|
-
raw = await
|
|
3243
|
+
raw = await fs3.readFile(file, "utf8");
|
|
3121
3244
|
} catch (err) {
|
|
3122
3245
|
if (err.code === "ENOENT") return emptyHqAuthFile();
|
|
3123
|
-
|
|
3124
|
-
|
|
3246
|
+
throw new Error(
|
|
3247
|
+
`HQ auth file at ${file} cannot be read: ${err.message}. Fix the file permissions or delete it to start with an empty auth state.`
|
|
3248
|
+
);
|
|
3125
3249
|
}
|
|
3250
|
+
let parsed;
|
|
3126
3251
|
try {
|
|
3127
|
-
|
|
3128
|
-
if (parsed.version !== HQ_AUTH_FILE_VERSION) {
|
|
3129
|
-
opts.warn?.(
|
|
3130
|
-
`HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}); ignoring stored policy/tokens.`
|
|
3131
|
-
);
|
|
3132
|
-
return emptyHqAuthFile();
|
|
3133
|
-
}
|
|
3134
|
-
return parsed;
|
|
3252
|
+
parsed = JSON.parse(raw);
|
|
3135
3253
|
} catch (err) {
|
|
3136
|
-
|
|
3137
|
-
|
|
3254
|
+
throw new Error(
|
|
3255
|
+
`HQ auth file at ${file} is not valid JSON: ${err.message}. Fix the file or delete it to start with an empty auth state.`
|
|
3256
|
+
);
|
|
3138
3257
|
}
|
|
3258
|
+
if (parsed.version !== HQ_AUTH_FILE_VERSION) {
|
|
3259
|
+
throw new Error(
|
|
3260
|
+
`HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}). Remove or update the file to match the current schema version.`
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
return parsed;
|
|
3139
3264
|
}
|
|
3140
3265
|
async function writeHqAuthFile(dataDir, file) {
|
|
3141
3266
|
const target = hqAuthFilePath(dataDir);
|
|
@@ -3150,7 +3275,7 @@ async function writeHqAuthFile(dataDir, file) {
|
|
|
3150
3275
|
async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
|
|
3151
3276
|
const file = hqAuthFilePath(dataDir);
|
|
3152
3277
|
try {
|
|
3153
|
-
await
|
|
3278
|
+
await fs3.access(file);
|
|
3154
3279
|
return { authFile: await readHqAuthFile(dataDir, opts), created: false };
|
|
3155
3280
|
} catch (err) {
|
|
3156
3281
|
if (err.code !== "ENOENT") {
|
|
@@ -3283,7 +3408,7 @@ function discoverLocalHqEndpoint(options = {}) {
|
|
|
3283
3408
|
}
|
|
3284
3409
|
function readFirstClientTokenFromAuthFile(dataDir) {
|
|
3285
3410
|
try {
|
|
3286
|
-
const raw =
|
|
3411
|
+
const raw = fs4.readFileSync(hqAuthFilePath(dataDir), "utf8");
|
|
3287
3412
|
const parsed = JSON.parse(raw);
|
|
3288
3413
|
return parsed.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
|
|
3289
3414
|
} catch {
|
|
@@ -3393,63 +3518,8 @@ function createGlobalMailbox(options) {
|
|
|
3393
3518
|
return new GlobalMailbox(options.projectDir, options.events, options.hqPublisher);
|
|
3394
3519
|
}
|
|
3395
3520
|
|
|
3396
|
-
// src/hq/agent-bridge.ts
|
|
3397
|
-
function startAgentMonitorEventBridge(opts) {
|
|
3398
|
-
const { events, clientId, projectId, publish } = opts;
|
|
3399
|
-
const seq = { current: 0 };
|
|
3400
|
-
function nextSeq() {
|
|
3401
|
-
seq.current += 1;
|
|
3402
|
-
return seq.current;
|
|
3403
|
-
}
|
|
3404
|
-
function buildEnvelope(type, payload, sessionId) {
|
|
3405
|
-
return createHqEventEnvelope({
|
|
3406
|
-
id: `${Date.now().toString(36)}-${nextSeq()}`,
|
|
3407
|
-
type,
|
|
3408
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3409
|
-
clientId,
|
|
3410
|
-
projectId,
|
|
3411
|
-
seq: nextSeq(),
|
|
3412
|
-
payload,
|
|
3413
|
-
// Carry the parent session id so HQ can scope per-agent transcripts by
|
|
3414
|
-
// (sessionId, subagentId) — without it, two sessions whose leaders both
|
|
3415
|
-
// use the default id 'leader' would collide into one shared ring.
|
|
3416
|
-
...sessionId !== void 0 ? { sessionId } : {}
|
|
3417
|
-
});
|
|
3418
|
-
}
|
|
3419
|
-
const offMessage = events.on("agent.timeline.message", (payload) => {
|
|
3420
|
-
if (!publish) return;
|
|
3421
|
-
const msgPayload = {
|
|
3422
|
-
subagentId: payload.subagentId,
|
|
3423
|
-
agentName: payload.agentName,
|
|
3424
|
-
content: payload.content,
|
|
3425
|
-
kind: payload.kind,
|
|
3426
|
-
iteration: payload.iteration,
|
|
3427
|
-
ts: payload.ts
|
|
3428
|
-
};
|
|
3429
|
-
if (payload.toolName !== void 0) msgPayload.toolName = payload.toolName;
|
|
3430
|
-
if (payload.costUsd !== void 0) msgPayload.costUsd = payload.costUsd;
|
|
3431
|
-
publish(buildEnvelope("agent.message", msgPayload, payload.sessionId));
|
|
3432
|
-
});
|
|
3433
|
-
const offStatus = events.on("agent.status_changed", (payload) => {
|
|
3434
|
-
if (!publish) return;
|
|
3435
|
-
const statusPayload = {
|
|
3436
|
-
subagentId: payload.subagentId,
|
|
3437
|
-
agentName: payload.agentName,
|
|
3438
|
-
status: payload.status,
|
|
3439
|
-
ts: payload.ts
|
|
3440
|
-
};
|
|
3441
|
-
if (payload.summary !== void 0) statusPayload.summary = payload.summary;
|
|
3442
|
-
if (payload.task !== void 0) statusPayload.task = payload.task;
|
|
3443
|
-
publish(buildEnvelope("agent.status", statusPayload));
|
|
3444
|
-
});
|
|
3445
|
-
return () => {
|
|
3446
|
-
offMessage();
|
|
3447
|
-
offStatus();
|
|
3448
|
-
};
|
|
3449
|
-
}
|
|
3450
|
-
|
|
3451
3521
|
// src/hq/session-bridge.ts
|
|
3452
|
-
import * as
|
|
3522
|
+
import * as fs5 from "node:fs";
|
|
3453
3523
|
import * as fsp2 from "node:fs/promises";
|
|
3454
3524
|
|
|
3455
3525
|
// src/hq/transcript-mapper.ts
|
|
@@ -3719,7 +3789,7 @@ function startSessionTelemetryBridge(opts) {
|
|
|
3719
3789
|
function setupWatcher() {
|
|
3720
3790
|
if (disposed || watcher) return;
|
|
3721
3791
|
try {
|
|
3722
|
-
const nextWatcher =
|
|
3792
|
+
const nextWatcher = fs5.watch(sessionFile, () => {
|
|
3723
3793
|
if (watchPending || disposed) return;
|
|
3724
3794
|
watchPending = true;
|
|
3725
3795
|
setTimeout(() => {
|
|
@@ -3816,7 +3886,11 @@ function startFleetTelemetryBridge(opts) {
|
|
|
3816
3886
|
const subagents = stats.subagentStatuses.map((s) => ({
|
|
3817
3887
|
subagentId: s.subagentId,
|
|
3818
3888
|
...s.taskId ? { task: s.taskId } : {},
|
|
3819
|
-
status: normalizeSubagentStatus(s.status)
|
|
3889
|
+
status: normalizeSubagentStatus(s.status),
|
|
3890
|
+
...s.model !== void 0 ? { model: s.model } : {},
|
|
3891
|
+
...s.costUsd !== void 0 ? { costUsd: s.costUsd } : {},
|
|
3892
|
+
...s.runtimeMs !== void 0 ? { runtimeMs: s.runtimeMs } : {},
|
|
3893
|
+
...s.lastActivityAt !== void 0 ? { lastActivityAt: s.lastActivityAt } : {}
|
|
3820
3894
|
}));
|
|
3821
3895
|
return {
|
|
3822
3896
|
runId,
|
|
@@ -3824,6 +3898,7 @@ function startFleetTelemetryBridge(opts) {
|
|
|
3824
3898
|
queuedTasks: stats.pending,
|
|
3825
3899
|
completedTasks: stats.completed,
|
|
3826
3900
|
failedTasks: stats.stopped,
|
|
3901
|
+
...stats.totalCostUsd !== void 0 ? { totalCostUsd: stats.totalCostUsd } : {},
|
|
3827
3902
|
subagents
|
|
3828
3903
|
};
|
|
3829
3904
|
}
|
|
@@ -4139,6 +4214,10 @@ function startCostTelemetryBridge(opts) {
|
|
|
4139
4214
|
totalTokens: p.usage.input + p.usage.output,
|
|
4140
4215
|
costUsd: p.cost.total
|
|
4141
4216
|
};
|
|
4217
|
+
if (p.provider !== void 0) payload.provider = p.provider;
|
|
4218
|
+
if (p.model !== void 0) payload.model = p.model;
|
|
4219
|
+
if (p.usage.cacheRead !== void 0) payload.cacheRead = p.usage.cacheRead;
|
|
4220
|
+
if (p.usage.cacheWrite !== void 0) payload.cacheWrite = p.usage.cacheWrite;
|
|
4142
4221
|
const sessionId = opts.sessionId ?? p.sessionId;
|
|
4143
4222
|
publisher.publishEvent({
|
|
4144
4223
|
type: "session.usage",
|
|
@@ -4155,7 +4234,7 @@ function startCostTelemetryBridge(opts) {
|
|
|
4155
4234
|
}
|
|
4156
4235
|
|
|
4157
4236
|
// src/hq/persistence.ts
|
|
4158
|
-
import * as
|
|
4237
|
+
import * as fs6 from "node:fs/promises";
|
|
4159
4238
|
import * as path6 from "node:path";
|
|
4160
4239
|
var DEFAULT_EVENT_LOG_MAX_LINES = 5e4;
|
|
4161
4240
|
var DEFAULT_EVENT_LOG_ROTATE_KEEP = 2e4;
|
|
@@ -4187,7 +4266,7 @@ var HqEventLog = class {
|
|
|
4187
4266
|
this.counted = true;
|
|
4188
4267
|
}
|
|
4189
4268
|
const line = JSON.stringify(event) + "\n";
|
|
4190
|
-
await
|
|
4269
|
+
await fs6.appendFile(this.filePath, line, { encoding: "utf8" });
|
|
4191
4270
|
this.lineCount += 1;
|
|
4192
4271
|
if (this.lineCount >= this.maxLines) {
|
|
4193
4272
|
await this.rotate();
|
|
@@ -4196,7 +4275,7 @@ var HqEventLog = class {
|
|
|
4196
4275
|
async rotate() {
|
|
4197
4276
|
await withFileLock(this.filePath, async () => {
|
|
4198
4277
|
try {
|
|
4199
|
-
const content = await
|
|
4278
|
+
const content = await fs6.readFile(this.filePath, "utf8");
|
|
4200
4279
|
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
4201
4280
|
if (lines.length <= this.rotateKeep) {
|
|
4202
4281
|
this.lineCount = lines.length;
|
|
@@ -4211,7 +4290,7 @@ var HqEventLog = class {
|
|
|
4211
4290
|
}
|
|
4212
4291
|
async countLines() {
|
|
4213
4292
|
try {
|
|
4214
|
-
const content = await
|
|
4293
|
+
const content = await fs6.readFile(this.filePath, "utf8");
|
|
4215
4294
|
return content.split("\n").filter((l) => l.length > 0).length;
|
|
4216
4295
|
} catch {
|
|
4217
4296
|
return 0;
|
|
@@ -4224,7 +4303,7 @@ var HqEventLog = class {
|
|
|
4224
4303
|
async recent(limit, typeFilter) {
|
|
4225
4304
|
let content;
|
|
4226
4305
|
try {
|
|
4227
|
-
content = await
|
|
4306
|
+
content = await fs6.readFile(this.filePath, "utf8");
|
|
4228
4307
|
} catch {
|
|
4229
4308
|
return [];
|
|
4230
4309
|
}
|
|
@@ -4266,7 +4345,7 @@ var HqSnapshotStore = class {
|
|
|
4266
4345
|
/** Read the last persisted snapshot, or `null` if none. */
|
|
4267
4346
|
async load() {
|
|
4268
4347
|
try {
|
|
4269
|
-
const content = await
|
|
4348
|
+
const content = await fs6.readFile(this.filePath, "utf8");
|
|
4270
4349
|
return JSON.parse(content);
|
|
4271
4350
|
} catch {
|
|
4272
4351
|
return null;
|
|
@@ -4300,6 +4379,21 @@ var HqTimeseriesStore = class {
|
|
|
4300
4379
|
if (signal.outputTokens !== void 0) bucket.outputTokens += signal.outputTokens;
|
|
4301
4380
|
if (signal.toolCalls !== void 0) bucket.toolCalls += signal.toolCalls;
|
|
4302
4381
|
if (signal.activeAgents !== void 0) bucket.activeAgents = signal.activeAgents;
|
|
4382
|
+
if (signal.model !== void 0 || signal.provider !== void 0) {
|
|
4383
|
+
const entry = {
|
|
4384
|
+
costUsd: signal.costUsd ?? 0,
|
|
4385
|
+
inputTokens: signal.inputTokens ?? 0,
|
|
4386
|
+
outputTokens: signal.outputTokens ?? 0,
|
|
4387
|
+
...signal.cacheRead !== void 0 ? { cacheRead: signal.cacheRead } : {},
|
|
4388
|
+
...signal.cacheWrite !== void 0 ? { cacheWrite: signal.cacheWrite } : {}
|
|
4389
|
+
};
|
|
4390
|
+
if (signal.model !== void 0) {
|
|
4391
|
+
bucket.byModel = foldBreakdown(bucket.byModel, signal.model, entry);
|
|
4392
|
+
}
|
|
4393
|
+
if (signal.provider !== void 0) {
|
|
4394
|
+
bucket.byProvider = foldBreakdown(bucket.byProvider, signal.provider, entry);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4303
4397
|
if (this.buckets.size > this.maxBuckets) {
|
|
4304
4398
|
const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
|
|
4305
4399
|
while (this.buckets.size > this.maxBuckets && sorted.length > 0) {
|
|
@@ -4324,7 +4418,7 @@ var HqTimeseriesStore = class {
|
|
|
4324
4418
|
async flushInternal(toWrite) {
|
|
4325
4419
|
const lines = toWrite.map((b) => JSON.stringify(b)).join("\n") + "\n";
|
|
4326
4420
|
await withFileLock(this.filePath, async () => {
|
|
4327
|
-
await
|
|
4421
|
+
await fs6.appendFile(this.filePath, lines, { encoding: "utf8" });
|
|
4328
4422
|
});
|
|
4329
4423
|
const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
|
|
4330
4424
|
while (this.buckets.size > this.maxBuckets) {
|
|
@@ -4343,7 +4437,7 @@ var HqTimeseriesStore = class {
|
|
|
4343
4437
|
async load() {
|
|
4344
4438
|
let content;
|
|
4345
4439
|
try {
|
|
4346
|
-
content = await
|
|
4440
|
+
content = await fs6.readFile(this.filePath, "utf8");
|
|
4347
4441
|
} catch {
|
|
4348
4442
|
return;
|
|
4349
4443
|
}
|
|
@@ -4364,11 +4458,66 @@ var HqTimeseriesStore = class {
|
|
|
4364
4458
|
}
|
|
4365
4459
|
}
|
|
4366
4460
|
};
|
|
4461
|
+
function foldBreakdown(map, key, entry) {
|
|
4462
|
+
const next = { ...map };
|
|
4463
|
+
const existing = next[key];
|
|
4464
|
+
if (existing === void 0) {
|
|
4465
|
+
next[key] = { ...entry };
|
|
4466
|
+
} else {
|
|
4467
|
+
next[key] = {
|
|
4468
|
+
costUsd: existing.costUsd + entry.costUsd,
|
|
4469
|
+
inputTokens: existing.inputTokens + entry.inputTokens,
|
|
4470
|
+
outputTokens: existing.outputTokens + entry.outputTokens,
|
|
4471
|
+
cacheRead: existing.cacheRead !== void 0 || entry.cacheRead !== void 0 ? (existing.cacheRead ?? 0) + (entry.cacheRead ?? 0) : void 0,
|
|
4472
|
+
cacheWrite: existing.cacheWrite !== void 0 || entry.cacheWrite !== void 0 ? (existing.cacheWrite ?? 0) + (entry.cacheWrite ?? 0) : void 0
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
return next;
|
|
4476
|
+
}
|
|
4477
|
+
var HqSimpleLog = class {
|
|
4478
|
+
filePath;
|
|
4479
|
+
writeChain = Promise.resolve();
|
|
4480
|
+
constructor(dataDir, filename) {
|
|
4481
|
+
this.filePath = path6.join(dataDir, filename);
|
|
4482
|
+
}
|
|
4483
|
+
/** Append one record as a JSON line. Best-effort, never rejects. */
|
|
4484
|
+
append(record) {
|
|
4485
|
+
this.writeChain = this.writeChain.then(() => fs6.appendFile(this.filePath, `${JSON.stringify(record)}
|
|
4486
|
+
`, { encoding: "utf8" })).catch(() => {
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
4489
|
+
/** Resolves once all queued appends have settled. For tests. */
|
|
4490
|
+
async drain() {
|
|
4491
|
+
await this.writeChain.catch(() => {
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
/** Read all records oldest-first. Returns `[]` if the file doesn't exist. */
|
|
4495
|
+
async readAll() {
|
|
4496
|
+
let content;
|
|
4497
|
+
try {
|
|
4498
|
+
content = await fs6.readFile(this.filePath, "utf8");
|
|
4499
|
+
} catch {
|
|
4500
|
+
return [];
|
|
4501
|
+
}
|
|
4502
|
+
const out = [];
|
|
4503
|
+
for (const line of content.split("\n")) {
|
|
4504
|
+
const trimmed = line.trim();
|
|
4505
|
+
if (!trimmed) continue;
|
|
4506
|
+
try {
|
|
4507
|
+
out.push(JSON.parse(trimmed));
|
|
4508
|
+
} catch {
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
return out;
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4367
4514
|
function createHqPersistence(dataDir) {
|
|
4368
4515
|
return {
|
|
4369
4516
|
eventLog: new HqEventLog({ dataDir }),
|
|
4370
4517
|
snapshotStore: new HqSnapshotStore({ dataDir }),
|
|
4371
|
-
timeseries: new HqTimeseriesStore({ dataDir })
|
|
4518
|
+
timeseries: new HqTimeseriesStore({ dataDir }),
|
|
4519
|
+
commandLog: new HqSimpleLog(dataDir, "commands.jsonl"),
|
|
4520
|
+
alertLog: new HqSimpleLog(dataDir, "alerts.jsonl")
|
|
4372
4521
|
};
|
|
4373
4522
|
}
|
|
4374
4523
|
|
|
@@ -4449,18 +4598,24 @@ function validateHqCommand(queued) {
|
|
|
4449
4598
|
var HqCommandAuditLog = class {
|
|
4450
4599
|
entries = [];
|
|
4451
4600
|
max;
|
|
4452
|
-
|
|
4601
|
+
onPersist;
|
|
4602
|
+
constructor(max = 1e3, onPersist) {
|
|
4453
4603
|
this.max = max;
|
|
4604
|
+
this.onPersist = onPersist;
|
|
4454
4605
|
}
|
|
4455
4606
|
record(entry) {
|
|
4456
4607
|
this.entries.push(entry);
|
|
4457
4608
|
if (this.entries.length > this.max) {
|
|
4458
4609
|
this.entries.splice(0, this.entries.length - this.max);
|
|
4459
4610
|
}
|
|
4611
|
+
this.onPersist?.(entry);
|
|
4460
4612
|
}
|
|
4461
4613
|
update(commandId, patch) {
|
|
4462
4614
|
const entry = this.entries.find((e) => e.commandId === commandId);
|
|
4463
|
-
if (entry)
|
|
4615
|
+
if (entry) {
|
|
4616
|
+
Object.assign(entry, patch);
|
|
4617
|
+
this.onPersist?.(entry);
|
|
4618
|
+
}
|
|
4464
4619
|
}
|
|
4465
4620
|
/** Update only when the command belongs to the authenticated client. */
|
|
4466
4621
|
updateForClient(commandId, clientId, patch) {
|
|
@@ -4469,8 +4624,18 @@ var HqCommandAuditLog = class {
|
|
|
4469
4624
|
);
|
|
4470
4625
|
if (!entry) return false;
|
|
4471
4626
|
Object.assign(entry, patch);
|
|
4627
|
+
this.onPersist?.(entry);
|
|
4472
4628
|
return true;
|
|
4473
4629
|
}
|
|
4630
|
+
/** Seed the ring from a durable store on boot (no persist callback fired). */
|
|
4631
|
+
seed(entries) {
|
|
4632
|
+
for (const entry of entries) {
|
|
4633
|
+
this.entries.push(entry);
|
|
4634
|
+
}
|
|
4635
|
+
if (this.entries.length > this.max) {
|
|
4636
|
+
this.entries.splice(0, this.entries.length - this.max);
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4474
4639
|
recent(limit = 200) {
|
|
4475
4640
|
return this.entries.slice(-limit);
|
|
4476
4641
|
}
|
|
@@ -4545,9 +4710,11 @@ var HqAlertEngine = class {
|
|
|
4545
4710
|
maxHistory;
|
|
4546
4711
|
timer = null;
|
|
4547
4712
|
onAlert;
|
|
4713
|
+
onPersist;
|
|
4548
4714
|
constructor(opts) {
|
|
4549
4715
|
this.onAlert = opts.onAlert;
|
|
4550
4716
|
this.maxHistory = opts.maxHistory ?? 500;
|
|
4717
|
+
this.onPersist = opts.onPersist;
|
|
4551
4718
|
}
|
|
4552
4719
|
/**
|
|
4553
4720
|
* Evaluate all rules against the snapshot. Emits (via the `onAlert`
|
|
@@ -4577,6 +4744,7 @@ var HqAlertEngine = class {
|
|
|
4577
4744
|
if (this.history.length > this.maxHistory) this.history.splice(0, this.history.length - this.maxHistory);
|
|
4578
4745
|
fired.push(alert);
|
|
4579
4746
|
this.onAlert(alert);
|
|
4747
|
+
this.onPersist?.(alert);
|
|
4580
4748
|
} else {
|
|
4581
4749
|
existing.lastFiredAt = now;
|
|
4582
4750
|
}
|
|
@@ -4597,6 +4765,15 @@ var HqAlertEngine = class {
|
|
|
4597
4765
|
recentAlerts(limit = 100) {
|
|
4598
4766
|
return this.history.slice(-limit);
|
|
4599
4767
|
}
|
|
4768
|
+
/** Seed the history from a durable store on boot (no alert callbacks fired). */
|
|
4769
|
+
seed(alerts) {
|
|
4770
|
+
for (const alert of alerts) {
|
|
4771
|
+
this.history.push(alert);
|
|
4772
|
+
}
|
|
4773
|
+
if (this.history.length > this.maxHistory) {
|
|
4774
|
+
this.history.splice(0, this.history.length - this.maxHistory);
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4600
4777
|
/**
|
|
4601
4778
|
* Start periodic evaluation against a snapshot getter. The timer is
|
|
4602
4779
|
* unref'd so it never keeps the process alive. Returns a disposer.
|
|
@@ -4638,6 +4815,7 @@ export {
|
|
|
4638
4815
|
HqCommandAuditLog,
|
|
4639
4816
|
HqEventLog,
|
|
4640
4817
|
HqPublisher,
|
|
4818
|
+
HqSimpleLog,
|
|
4641
4819
|
HqSnapshotStore,
|
|
4642
4820
|
HqTimeseriesStore,
|
|
4643
4821
|
buildTranscriptFromEvents,
|
|
@@ -4674,7 +4852,6 @@ export {
|
|
|
4674
4852
|
resolveHqDataDir,
|
|
4675
4853
|
resolveHqRedactionPolicy,
|
|
4676
4854
|
scrubAndTruncateHqPreview,
|
|
4677
|
-
startAgentMonitorEventBridge,
|
|
4678
4855
|
startBrainTelemetryBridge,
|
|
4679
4856
|
startCostTelemetryBridge,
|
|
4680
4857
|
startFleetTelemetryBridge,
|