hoomanjs 1.58.1 → 1.60.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.
Files changed (100) hide show
  1. package/README.md +2 -1
  2. package/dist/acp/acp-agent.js +17 -3
  3. package/dist/acp/acp-agent.js.map +1 -1
  4. package/dist/acp/approvals.js +11 -0
  5. package/dist/acp/approvals.js.map +1 -1
  6. package/dist/acp/meta/daemon.d.ts +12 -0
  7. package/dist/acp/meta/daemon.js +16 -0
  8. package/dist/acp/meta/daemon.js.map +1 -0
  9. package/dist/acp/meta/origin.d.ts +8 -0
  10. package/dist/acp/meta/origin.js +38 -0
  11. package/dist/acp/meta/origin.js.map +1 -0
  12. package/dist/acp/sessions/store.d.ts +6 -0
  13. package/dist/acp/sessions/store.js.map +1 -1
  14. package/dist/chat/components/StatusBar.js +1 -25
  15. package/dist/chat/components/StatusBar.js.map +1 -1
  16. package/dist/cli.js +143 -26
  17. package/dist/cli.js.map +1 -1
  18. package/dist/configure/app.js +1 -0
  19. package/dist/configure/app.js.map +1 -1
  20. package/dist/core/config.d.ts +29 -0
  21. package/dist/core/config.js +32 -0
  22. package/dist/core/config.js.map +1 -1
  23. package/dist/core/index.d.ts +7 -0
  24. package/dist/core/index.js +2 -1
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/mcp/manager.d.ts +15 -0
  27. package/dist/core/mcp/manager.js +28 -0
  28. package/dist/core/mcp/manager.js.map +1 -1
  29. package/dist/core/mcp/prefixed-mcp-tool.d.ts +1 -0
  30. package/dist/core/mcp/prefixed-mcp-tool.js +1 -1
  31. package/dist/core/mcp/prefixed-mcp-tool.js.map +1 -1
  32. package/dist/core/skills/built-in/hooman-channels/SKILL.md +6 -6
  33. package/dist/core/skills/built-in/hooman-config/SKILL.md +35 -3
  34. package/dist/core/utils/usage-format.d.ts +4 -0
  35. package/dist/core/utils/usage-format.js +27 -0
  36. package/dist/core/utils/usage-format.js.map +1 -0
  37. package/dist/daemon/acp-client.d.ts +53 -0
  38. package/dist/daemon/acp-client.js +132 -0
  39. package/dist/daemon/acp-client.js.map +1 -0
  40. package/dist/daemon/approvals.d.ts +9 -2
  41. package/dist/daemon/approvals.js +151 -6
  42. package/dist/daemon/approvals.js.map +1 -1
  43. package/dist/daemon/attachments.d.ts +9 -0
  44. package/dist/daemon/attachments.js +63 -0
  45. package/dist/daemon/attachments.js.map +1 -0
  46. package/dist/daemon/dashboard/store.d.ts +36 -0
  47. package/dist/daemon/dashboard/store.js +290 -0
  48. package/dist/daemon/dashboard/store.js.map +1 -0
  49. package/dist/daemon/dashboard/types.d.ts +60 -0
  50. package/dist/daemon/dashboard/types.js +2 -0
  51. package/dist/daemon/dashboard/types.js.map +1 -0
  52. package/dist/daemon/index.d.ts +19 -6
  53. package/dist/daemon/index.js +181 -74
  54. package/dist/daemon/index.js.map +1 -1
  55. package/dist/daemon/mcproxy/catalog.d.ts +17 -0
  56. package/dist/daemon/mcproxy/catalog.js +71 -0
  57. package/dist/daemon/mcproxy/catalog.js.map +1 -0
  58. package/dist/daemon/mcproxy/http-server.d.ts +15 -0
  59. package/dist/daemon/mcproxy/http-server.js +150 -0
  60. package/dist/daemon/mcproxy/http-server.js.map +1 -0
  61. package/dist/daemon/mcproxy/index.d.ts +23 -0
  62. package/dist/daemon/mcproxy/index.js +23 -0
  63. package/dist/daemon/mcproxy/index.js.map +1 -0
  64. package/dist/daemon/mcproxy/session.d.ts +9 -0
  65. package/dist/daemon/mcproxy/session.js +24 -0
  66. package/dist/daemon/mcproxy/session.js.map +1 -0
  67. package/dist/daemon/queue.d.ts +14 -5
  68. package/dist/daemon/queue.js +40 -33
  69. package/dist/daemon/queue.js.map +1 -1
  70. package/dist/daemon/session-registry.d.ts +112 -0
  71. package/dist/daemon/session-registry.js +272 -0
  72. package/dist/daemon/session-registry.js.map +1 -0
  73. package/dist/daemon/session-store.d.ts +20 -0
  74. package/dist/daemon/session-store.js +63 -0
  75. package/dist/daemon/session-store.js.map +1 -0
  76. package/dist/daemon/ui/Card.d.ts +8 -0
  77. package/dist/daemon/ui/Card.js +52 -0
  78. package/dist/daemon/ui/Card.js.map +1 -0
  79. package/dist/daemon/ui/DaemonDashboardApp.d.ts +6 -0
  80. package/dist/daemon/ui/DaemonDashboardApp.js +75 -0
  81. package/dist/daemon/ui/DaemonDashboardApp.js.map +1 -0
  82. package/dist/daemon/ui/Footer.d.ts +3 -0
  83. package/dist/daemon/ui/Footer.js +7 -0
  84. package/dist/daemon/ui/Footer.js.map +1 -0
  85. package/dist/daemon/ui/Header.d.ts +4 -0
  86. package/dist/daemon/ui/Header.js +22 -0
  87. package/dist/daemon/ui/Header.js.map +1 -0
  88. package/dist/daemon/ui/Lane.d.ts +11 -0
  89. package/dist/daemon/ui/Lane.js +10 -0
  90. package/dist/daemon/ui/Lane.js.map +1 -0
  91. package/dist/daemon/ui/format.d.ts +7 -0
  92. package/dist/daemon/ui/format.js +67 -0
  93. package/dist/daemon/ui/format.js.map +1 -0
  94. package/dist/daemon/ui/index.d.ts +11 -0
  95. package/dist/daemon/ui/index.js +14 -0
  96. package/dist/daemon/ui/index.js.map +1 -0
  97. package/package.json +1 -1
  98. package/dist/daemon/questions.d.ts +0 -18
  99. package/dist/daemon/questions.js +0 -44
  100. package/dist/daemon/questions.js.map +0 -1
@@ -0,0 +1,23 @@
1
+ import { DaemonToolCatalog } from "./catalog.js";
2
+ import { startDaemonMcpHttpServer } from "./http-server.js";
3
+ import { createDaemonProxyServer } from "./session.js";
4
+ /**
5
+ * Starts the daemon's local, loopback-only aggregate MCP tool proxy in front
6
+ * of the parent `Manager`'s already-connected upstream servers. Call once,
7
+ * after the parent has connected/subscribed to channels and before creating
8
+ * any ACP session; every daemon-hosted ACP session gets the one returned
9
+ * `mcpServer` entry instead of loading local `mcp.json`.
10
+ */
11
+ export async function startDaemonMcpProxy(manager, options = {}) {
12
+ const catalog = new DaemonToolCatalog(manager);
13
+ const instructions = (await manager.listServerInstructions()).join("\n\n");
14
+ const handle = await startDaemonMcpHttpServer({
15
+ port: options.port ?? 0,
16
+ createSession: () => createDaemonProxyServer(catalog, instructions),
17
+ });
18
+ return {
19
+ mcpServer: { name: "daemon", type: "http", url: handle.url, headers: [] },
20
+ close: handle.close,
21
+ };
22
+ }
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/daemon/mcproxy/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAgBvD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAmB,EACnB,UAA6B,EAAE;IAE/B,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,CAAC,MAAM,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC;QAC5C,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC;QACvB,aAAa,EAAE,GAAG,EAAE,CAAC,uBAAuB,CAAC,OAAO,EAAE,YAAY,CAAC;KACpE,CAAC,CAAC;IACH,OAAO;QACL,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE;QACzE,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import type { DaemonToolCatalog } from "./catalog.js";
3
+ /**
4
+ * One frontend MCP session for a daemon-hosted ACP child: a fresh low-level
5
+ * `Server` per HTTP initialize handshake, exposing only the aggregate tool
6
+ * catalog (no experimental channel capabilities — those stay on the parent's
7
+ * singleton upstream connections).
8
+ */
9
+ export declare function createDaemonProxyServer(catalog: DaemonToolCatalog, instructions: string): Server;
@@ -0,0 +1,24 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ const packageUrl = new URL("../../../package.json", import.meta.url);
5
+ const packageVersion = JSON.parse(readFileSync(packageUrl, "utf8"))
6
+ .version ?? "0.0.0";
7
+ /**
8
+ * One frontend MCP session for a daemon-hosted ACP child: a fresh low-level
9
+ * `Server` per HTTP initialize handshake, exposing only the aggregate tool
10
+ * catalog (no experimental channel capabilities — those stay on the parent's
11
+ * singleton upstream connections).
12
+ */
13
+ export function createDaemonProxyServer(catalog, instructions) {
14
+ const server = new Server({ name: "hooman-daemon-mcproxy", version: packageVersion }, {
15
+ capabilities: { tools: {} },
16
+ instructions: instructions.length > 0 ? instructions : undefined,
17
+ });
18
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
+ tools: await catalog.listTools(),
20
+ }));
21
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => catalog.callTool(request.params.name, request.params.arguments, extra.signal));
22
+ return server;
23
+ }
24
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/daemon/mcproxy/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAG5C,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrE,MAAM,cAAc,GACjB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAA0B;KACnE,OAAO,IAAI,OAAO,CAAC;AAExB;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAA0B,EAC1B,YAAoB;IAEpB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,cAAc,EAAE,EAC1D;QACE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAC3B,YAAY,EAAE,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;KACjE,CACF,CAAC;IACF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE,MAAM,OAAO,CAAC,SAAS,EAAE;KACjC,CAAC,CAAC,CAAC;IACJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CACvE,OAAO,CAAC,QAAQ,CACd,OAAO,CAAC,MAAM,CAAC,IAAI,EACnB,OAAO,CAAC,MAAM,CAAC,SAAS,EACxB,KAAK,CAAC,MAAM,CACb,CACF,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,5 +1,14 @@
1
- import fastq from "fastq";
2
- import type { ChannelMessage } from "../core/mcp/index.js";
3
- type MessageQueue = fastq.queueAsPromised<ChannelMessage, void>;
4
- export declare function createQueue(handler: (message: ChannelMessage) => Promise<void>, cleanup: () => void): Promise<[MessageQueue, () => Promise<void>, () => void]>;
5
- export {};
1
+ /**
2
+ * Serializes turns per external conversation key while allowing unrelated
3
+ * keys to run fully concurrently — replaces the old single global-concurrency
4
+ * queue now that many ACP sessions may be active at once.
5
+ */
6
+ export declare class KeyedTurnQueue {
7
+ #private;
8
+ /** Number of tasks currently queued (including the one running, if any) for `key`. */
9
+ length(key: string): number;
10
+ /** Enqueues `task` behind any already-running/queued task for the same `key`. */
11
+ push(key: string, task: () => Promise<void>): void;
12
+ /** Waits for every currently queued task (across every key) to settle. */
13
+ drain(): Promise<void>;
14
+ }
@@ -1,39 +1,46 @@
1
- import fastq from "fastq";
2
- export async function createQueue(handler, cleanup) {
3
- let stopping = false;
4
- let resolver = null;
5
- const queue = fastq.promise(async (message) => {
6
- await handler(message);
7
- }, 1);
8
- const stopper = new Promise((resolve) => {
9
- resolver = resolve;
10
- });
11
- const shutdown = () => {
12
- if (stopping) {
13
- return;
14
- }
15
- stopping = true;
16
- queue.kill();
17
- resolver?.();
18
- };
19
- const onSigInt = () => shutdown();
20
- const onSigTerm = () => shutdown();
21
- process.on("SIGINT", onSigInt);
22
- process.on("SIGTERM", onSigTerm);
23
- return [
24
- queue,
25
- async () => {
1
+ /**
2
+ * Serializes turns per external conversation key while allowing unrelated
3
+ * keys to run fully concurrently — replaces the old single global-concurrency
4
+ * queue now that many ACP sessions may be active at once.
5
+ */
6
+ export class KeyedTurnQueue {
7
+ #tails = new Map();
8
+ /** Number of tasks currently queued (including the one running, if any) for `key`. */
9
+ length(key) {
10
+ return this.#tails.get(key)?.length ?? 0;
11
+ }
12
+ /** Enqueues `task` behind any already-running/queued task for the same `key`. */
13
+ push(key, task) {
14
+ const state = this.#tails.get(key) ?? {
15
+ length: 0,
16
+ tail: Promise.resolve(),
17
+ };
18
+ state.length += 1;
19
+ this.#tails.set(key, state);
20
+ // Swallow task rejections here: `state.tail` is chained with `.then()`,
21
+ // so a rejected tail would skip every subsequent `.then()` callback for
22
+ // this key, permanently poisoning the queue instead of just failing one
23
+ // turn. Callers that care about per-task failure (e.g. `runTurn`) already
24
+ // catch their own errors; this is a backstop against anything that slips
25
+ // past that.
26
+ state.tail = state.tail.then(async () => {
26
27
  try {
27
- await stopper;
28
+ await task();
29
+ }
30
+ catch {
31
+ /* ignore: never poison the chain for this key */
28
32
  }
29
33
  finally {
30
- cleanup();
31
- await queue.drained().catch(() => { });
32
- process.off("SIGINT", onSigInt);
33
- process.off("SIGTERM", onSigTerm);
34
+ state.length -= 1;
35
+ if (state.length === 0) {
36
+ this.#tails.delete(key);
37
+ }
34
38
  }
35
- },
36
- shutdown,
37
- ];
39
+ });
40
+ }
41
+ /** Waits for every currently queued task (across every key) to settle. */
42
+ async drain() {
43
+ await Promise.allSettled([...this.#tails.values()].map((s) => s.tail));
44
+ }
38
45
  }
39
46
  //# sourceMappingURL=queue.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/daemon/queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAmD,EACnD,OAAmB;IAEnB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,QAAQ,GAAwB,IAAI,CAAC;IACzC,MAAM,KAAK,GAAiB,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,OAAuB,EAAE,EAAE;QAC1E,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,CAAC,CAAC;IAEN,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAC5C,QAAQ,GAAG,OAAO,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QACD,QAAQ,GAAG,IAAI,CAAC;QAChB,KAAK,CAAC,IAAI,EAAE,CAAC;QACb,QAAQ,EAAE,EAAE,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAClC,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAEnC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAEjC,OAAO;QACL,KAAK;QACL,KAAK,IAAI,EAAE;YACT,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC;YAChB,CAAC;oBAAS,CAAC;gBACT,OAAO,EAAE,CAAC;gBACV,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACtC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBAChC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QACD,QAAQ;KACT,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/daemon/queue.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,cAAc;IACzB,MAAM,GAAG,IAAI,GAAG,EAAmD,CAAC;IAEpE,sFAAsF;IAC/E,MAAM,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,iFAAiF;IAC1E,IAAI,CAAC,GAAW,EAAE,IAAyB;QAChD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI;YACpC,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE;SACxB,CAAC;QACF,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5B,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,0EAA0E;QAC1E,yEAAyE;QACzE,aAAa;QACb,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACtC,IAAI,CAAC;gBACH,MAAM,IAAI,EAAE,CAAC;YACf,CAAC;YAAC,MAAM,CAAC;gBACP,iDAAiD;YACnD,CAAC;oBAAS,CAAC;gBACT,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;gBAClB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACvB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,0EAA0E;IACnE,KAAK,CAAC,KAAK;QAChB,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACzE,CAAC;CACF"}
@@ -0,0 +1,112 @@
1
+ import type { ChannelOrigin } from "../core/approvals/channel-ask.js";
2
+ /** Why a runtime session's pool slot was released. */
3
+ export type DaemonDisposeReason = "idle_timeout" | "pool_pressure" | "shutdown" | "child_exit";
4
+ export type DaemonRegistryEvent = {
5
+ type: "slot_waiting";
6
+ externalKey: string;
7
+ } | {
8
+ type: "slot_acquired";
9
+ externalKey: string;
10
+ } | {
11
+ type: "active";
12
+ externalKey: string;
13
+ acpSessionId: string;
14
+ userId?: string;
15
+ } | {
16
+ type: "idle";
17
+ externalKey: string;
18
+ } | {
19
+ type: "disposed";
20
+ externalKey: string;
21
+ acpSessionId: string;
22
+ reason: DaemonDisposeReason;
23
+ };
24
+ export type DaemonSessionRegistryOptions = {
25
+ /** Bound on concurrently active ACP sessions. Must be a positive integer. */
26
+ maxActiveSessions: number;
27
+ /** Ordinary idle-close delay after a session becomes non-busy. `0` disables it (pressure eviction still applies). */
28
+ idleTimeoutMs: number;
29
+ /** Called to close an ACP session (idle timeout, pool pressure, or shutdown). May throw; cleanup still runs. */
30
+ onClose: (externalKey: string, acpSessionId: string) => Promise<void>;
31
+ /** Notified on every pool/lifecycle transition, for dashboard/diagnostics consumers. */
32
+ onEvent?: (event: DaemonRegistryEvent) => void;
33
+ };
34
+ /**
35
+ * Tracks the daemon's channel-conversation → ACP-session bindings, the
36
+ * durable JSONL mapping, and a bounded pool of concurrently active ACP
37
+ * sessions: idle sessions are evicted LRU under pressure, callers otherwise
38
+ * wait FIFO for a slot, and a non-busy session with pool waiters closes
39
+ * immediately instead of waiting out the ordinary idle timeout.
40
+ */
41
+ export declare class DaemonSessionRegistry {
42
+ #private;
43
+ private readonly options;
44
+ constructor(options: DaemonSessionRegistryOptions);
45
+ hydrate(): Promise<void>;
46
+ /** Persisted ACP session ID for a key from a prior daemon run, if any. */
47
+ persistedAcpSessionId(externalKey: string): string | undefined;
48
+ isActive(externalKey: string): boolean;
49
+ /** Whether an ACP session is already registered (created/resumed) for `externalKey` in this process. */
50
+ hasRuntime(externalKey: string): boolean;
51
+ acpSessionIdFor(externalKey: string): string | undefined;
52
+ /**
53
+ * Acquires one pool slot for `externalKey`. Reuses the caller's own slot
54
+ * if already held. Otherwise joins the FIFO waiter queue and, if a slot
55
+ * exists, immediately requests one idle LRU session be evicted to make
56
+ * room — but never grants the freed slot directly, so fairness holds even
57
+ * when eviction and waiting happen concurrently.
58
+ */
59
+ acquireSlot(externalKey: string): Promise<void>;
60
+ /**
61
+ * Releases a pool slot acquired via {@link acquireSlot} when session setup
62
+ * fails *before* a runtime was ever registered — otherwise the key would
63
+ * stay parked in `#activeKeys` forever (no runtime for `markIdle` to find),
64
+ * slowly exhausting the pool across repeated failures.
65
+ */
66
+ releaseFailedSlot(externalKey: string): void;
67
+ /**
68
+ * Drops every runtime entry (but keeps pool slots reserved) after the
69
+ * shared ACP child process exits unexpectedly. `hasRuntime()` then returns
70
+ * `false` for every key, so the next turn for each falls back to
71
+ * `resumeSession()` against the persisted ACP session ID on the fresh
72
+ * child, instead of silently prompting a runtime the new child never
73
+ * created.
74
+ */
75
+ invalidateRuntimes(): void;
76
+ /** Registers a newly created/resumed active session and marks it busy for the in-flight turn. */
77
+ registerActive(binding: {
78
+ externalKey: string;
79
+ acpSessionId: string;
80
+ cwd: string;
81
+ userId?: string;
82
+ origin: ChannelOrigin | null;
83
+ }): void;
84
+ /** Persists the external-key → ACP-session-ID binding (create, or replacement after a missing-session resume failure). */
85
+ persistBinding(params: {
86
+ externalKey: string;
87
+ acpSessionId: string;
88
+ cwd: string;
89
+ userId?: string | null;
90
+ }): Promise<void>;
91
+ updateOrigin(externalKey: string, origin: ChannelOrigin | null): void;
92
+ originForAcpSession(acpSessionId: string): ChannelOrigin | null;
93
+ externalKeyForAcpSession(acpSessionId: string): string | undefined;
94
+ /** Marks a session busy for the duration of one `session/prompt` call, cancelling any armed idle timer. */
95
+ markBusy(externalKey: string): void;
96
+ /**
97
+ * Marks a session non-busy after its turn settles. When more work for the
98
+ * same key is already queued, the caller keeps its slot untouched. When
99
+ * pool waiters exist, the session closes immediately instead of arming the
100
+ * ordinary idle timer.
101
+ */
102
+ markIdle(externalKey: string, hasQueuedWork: boolean): void;
103
+ /** Rejects queued waiters, closes every active session, and stops granting new slots. */
104
+ shutdown(): Promise<void>;
105
+ /** Snapshot pool occupancy for dashboard/diagnostics consumers. */
106
+ poolStats(): {
107
+ active: number;
108
+ max: number;
109
+ waiting: number;
110
+ };
111
+ get isShuttingDown(): boolean;
112
+ }
@@ -0,0 +1,272 @@
1
+ import { patchDaemonSessionBinding, readDaemonSessionBindings, } from "./session-store.js";
2
+ /**
3
+ * Tracks the daemon's channel-conversation → ACP-session bindings, the
4
+ * durable JSONL mapping, and a bounded pool of concurrently active ACP
5
+ * sessions: idle sessions are evicted LRU under pressure, callers otherwise
6
+ * wait FIFO for a slot, and a non-busy session with pool waiters closes
7
+ * immediately instead of waiting out the ordinary idle timeout.
8
+ */
9
+ export class DaemonSessionRegistry {
10
+ options;
11
+ #persisted = new Map();
12
+ #runtime = new Map();
13
+ #reverse = new Map();
14
+ #activeKeys = new Set();
15
+ #waiters = [];
16
+ #shuttingDown = false;
17
+ constructor(options) {
18
+ this.options = options;
19
+ if (!Number.isInteger(options.maxActiveSessions) ||
20
+ options.maxActiveSessions < 1) {
21
+ throw new Error("maxActiveSessions must be a positive integer.");
22
+ }
23
+ }
24
+ async hydrate() {
25
+ this.#persisted = await readDaemonSessionBindings();
26
+ }
27
+ /** Persisted ACP session ID for a key from a prior daemon run, if any. */
28
+ persistedAcpSessionId(externalKey) {
29
+ return this.#persisted.get(externalKey)?.acpSessionId;
30
+ }
31
+ isActive(externalKey) {
32
+ return this.#activeKeys.has(externalKey);
33
+ }
34
+ /** Whether an ACP session is already registered (created/resumed) for `externalKey` in this process. */
35
+ hasRuntime(externalKey) {
36
+ return this.#runtime.has(externalKey);
37
+ }
38
+ acpSessionIdFor(externalKey) {
39
+ return this.#runtime.get(externalKey)?.acpSessionId;
40
+ }
41
+ /**
42
+ * Acquires one pool slot for `externalKey`. Reuses the caller's own slot
43
+ * if already held. Otherwise joins the FIFO waiter queue and, if a slot
44
+ * exists, immediately requests one idle LRU session be evicted to make
45
+ * room — but never grants the freed slot directly, so fairness holds even
46
+ * when eviction and waiting happen concurrently.
47
+ */
48
+ async acquireSlot(externalKey) {
49
+ if (this.#activeKeys.has(externalKey)) {
50
+ return;
51
+ }
52
+ if (this.#activeKeys.size < this.options.maxActiveSessions) {
53
+ this.#activeKeys.add(externalKey);
54
+ this.options.onEvent?.({ type: "slot_acquired", externalKey });
55
+ return;
56
+ }
57
+ this.options.onEvent?.({ type: "slot_waiting", externalKey });
58
+ const waiterPromise = new Promise((resolve) => {
59
+ this.#waiters.push({ key: externalKey, resolve });
60
+ });
61
+ const idleKey = this.#pickIdleLru();
62
+ if (idleKey) {
63
+ void this.#closeRuntime(idleKey, "pool_pressure");
64
+ }
65
+ await waiterPromise;
66
+ this.options.onEvent?.({ type: "slot_acquired", externalKey });
67
+ }
68
+ /**
69
+ * Releases a pool slot acquired via {@link acquireSlot} when session setup
70
+ * fails *before* a runtime was ever registered — otherwise the key would
71
+ * stay parked in `#activeKeys` forever (no runtime for `markIdle` to find),
72
+ * slowly exhausting the pool across repeated failures.
73
+ */
74
+ releaseFailedSlot(externalKey) {
75
+ if (this.#runtime.has(externalKey)) {
76
+ return;
77
+ }
78
+ if (!this.#activeKeys.delete(externalKey)) {
79
+ return;
80
+ }
81
+ const waiter = this.#waiters.shift();
82
+ if (waiter) {
83
+ this.#activeKeys.add(waiter.key);
84
+ waiter.resolve();
85
+ }
86
+ }
87
+ /**
88
+ * Drops every runtime entry (but keeps pool slots reserved) after the
89
+ * shared ACP child process exits unexpectedly. `hasRuntime()` then returns
90
+ * `false` for every key, so the next turn for each falls back to
91
+ * `resumeSession()` against the persisted ACP session ID on the fresh
92
+ * child, instead of silently prompting a runtime the new child never
93
+ * created.
94
+ */
95
+ invalidateRuntimes() {
96
+ for (const runtime of this.#runtime.values()) {
97
+ if (runtime.idleTimer) {
98
+ clearTimeout(runtime.idleTimer);
99
+ }
100
+ this.options.onEvent?.({
101
+ type: "disposed",
102
+ externalKey: runtime.externalKey,
103
+ acpSessionId: runtime.acpSessionId,
104
+ reason: "child_exit",
105
+ });
106
+ }
107
+ this.#runtime.clear();
108
+ this.#reverse.clear();
109
+ }
110
+ /** Registers a newly created/resumed active session and marks it busy for the in-flight turn. */
111
+ registerActive(binding) {
112
+ this.#runtime.set(binding.externalKey, {
113
+ externalKey: binding.externalKey,
114
+ acpSessionId: binding.acpSessionId,
115
+ cwd: binding.cwd,
116
+ userId: binding.userId,
117
+ origin: binding.origin,
118
+ busy: true,
119
+ closing: false,
120
+ lastActiveAt: Date.now(),
121
+ idleTimer: null,
122
+ });
123
+ this.#reverse.set(binding.acpSessionId, binding.externalKey);
124
+ this.options.onEvent?.({
125
+ type: "active",
126
+ externalKey: binding.externalKey,
127
+ acpSessionId: binding.acpSessionId,
128
+ userId: binding.userId,
129
+ });
130
+ }
131
+ /** Persists the external-key → ACP-session-ID binding (create, or replacement after a missing-session resume failure). */
132
+ async persistBinding(params) {
133
+ const now = new Date().toISOString();
134
+ const existing = this.#persisted.get(params.externalKey);
135
+ const entry = {
136
+ externalKey: params.externalKey,
137
+ acpSessionId: params.acpSessionId,
138
+ cwd: params.cwd,
139
+ userId: params.userId ?? null,
140
+ createdAt: existing?.createdAt ?? now,
141
+ updatedAt: now,
142
+ };
143
+ this.#persisted.set(params.externalKey, entry);
144
+ await patchDaemonSessionBinding(entry);
145
+ }
146
+ updateOrigin(externalKey, origin) {
147
+ const runtime = this.#runtime.get(externalKey);
148
+ if (runtime) {
149
+ runtime.origin = origin;
150
+ }
151
+ }
152
+ originForAcpSession(acpSessionId) {
153
+ const key = this.#reverse.get(acpSessionId);
154
+ if (!key) {
155
+ return null;
156
+ }
157
+ return this.#runtime.get(key)?.origin ?? null;
158
+ }
159
+ externalKeyForAcpSession(acpSessionId) {
160
+ return this.#reverse.get(acpSessionId);
161
+ }
162
+ /** Marks a session busy for the duration of one `session/prompt` call, cancelling any armed idle timer. */
163
+ markBusy(externalKey) {
164
+ const runtime = this.#runtime.get(externalKey);
165
+ if (!runtime) {
166
+ return;
167
+ }
168
+ runtime.busy = true;
169
+ if (runtime.idleTimer) {
170
+ clearTimeout(runtime.idleTimer);
171
+ runtime.idleTimer = null;
172
+ }
173
+ }
174
+ /**
175
+ * Marks a session non-busy after its turn settles. When more work for the
176
+ * same key is already queued, the caller keeps its slot untouched. When
177
+ * pool waiters exist, the session closes immediately instead of arming the
178
+ * ordinary idle timer.
179
+ */
180
+ markIdle(externalKey, hasQueuedWork) {
181
+ const runtime = this.#runtime.get(externalKey);
182
+ if (!runtime || runtime.closing) {
183
+ return;
184
+ }
185
+ runtime.busy = false;
186
+ runtime.lastActiveAt = Date.now();
187
+ if (hasQueuedWork) {
188
+ return;
189
+ }
190
+ this.options.onEvent?.({ type: "idle", externalKey });
191
+ if (this.#waiters.length > 0) {
192
+ void this.#closeRuntime(externalKey, "pool_pressure");
193
+ return;
194
+ }
195
+ if (this.options.idleTimeoutMs > 0) {
196
+ runtime.idleTimer = setTimeout(() => {
197
+ runtime.idleTimer = null;
198
+ if (!runtime.busy && !runtime.closing) {
199
+ void this.#closeRuntime(externalKey, "idle_timeout");
200
+ }
201
+ }, this.options.idleTimeoutMs);
202
+ runtime.idleTimer.unref?.();
203
+ }
204
+ }
205
+ #pickIdleLru() {
206
+ let best;
207
+ for (const runtime of this.#runtime.values()) {
208
+ if (runtime.busy || runtime.closing) {
209
+ continue;
210
+ }
211
+ if (!best || runtime.lastActiveAt < best.lastActiveAt) {
212
+ best = runtime;
213
+ }
214
+ }
215
+ return best?.externalKey;
216
+ }
217
+ async #closeRuntime(externalKey, reason) {
218
+ const runtime = this.#runtime.get(externalKey);
219
+ if (!runtime || runtime.closing) {
220
+ return;
221
+ }
222
+ runtime.closing = true;
223
+ if (runtime.idleTimer) {
224
+ clearTimeout(runtime.idleTimer);
225
+ runtime.idleTimer = null;
226
+ }
227
+ try {
228
+ await this.options.onClose(externalKey, runtime.acpSessionId);
229
+ }
230
+ catch {
231
+ /* `onClose` is documented not to throw, but cleanup below must still
232
+ * run so the slot and waiter queue never leak on a misbehaving caller. */
233
+ }
234
+ finally {
235
+ this.#runtime.delete(externalKey);
236
+ this.#reverse.delete(runtime.acpSessionId);
237
+ this.#activeKeys.delete(externalKey);
238
+ this.options.onEvent?.({
239
+ type: "disposed",
240
+ externalKey,
241
+ acpSessionId: runtime.acpSessionId,
242
+ reason,
243
+ });
244
+ const waiter = this.#waiters.shift();
245
+ if (waiter) {
246
+ this.#activeKeys.add(waiter.key);
247
+ waiter.resolve();
248
+ }
249
+ }
250
+ }
251
+ /** Rejects queued waiters, closes every active session, and stops granting new slots. */
252
+ async shutdown() {
253
+ this.#shuttingDown = true;
254
+ const pending = this.#waiters.splice(0, this.#waiters.length);
255
+ for (const waiter of pending) {
256
+ waiter.resolve();
257
+ }
258
+ await Promise.all([...this.#runtime.keys()].map((key) => this.#closeRuntime(key, "shutdown")));
259
+ }
260
+ /** Snapshot pool occupancy for dashboard/diagnostics consumers. */
261
+ poolStats() {
262
+ return {
263
+ active: this.#activeKeys.size,
264
+ max: this.options.maxActiveSessions,
265
+ waiting: this.#waiters.length,
266
+ };
267
+ }
268
+ get isShuttingDown() {
269
+ return this.#shuttingDown;
270
+ }
271
+ }
272
+ //# sourceMappingURL=session-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../src/daemon/session-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Persisted external-conversation-key → ACP-session-ID binding, so daemon
3
+ * restarts can `session/resume` prior conversations instead of losing them.
4
+ * Append-only JSONL patch log (same fold/tombstone shape as
5
+ * `src/acp/sessions/store.ts`), scoped to the current project.
6
+ */
7
+ export type DaemonSessionBinding = {
8
+ externalKey: string;
9
+ acpSessionId: string;
10
+ cwd: string;
11
+ userId?: string | null;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ };
15
+ /** Reads and folds the append-only bindings log; unparseable (torn) lines are skipped. */
16
+ export declare function readDaemonSessionBindings(): Promise<Map<string, DaemonSessionBinding>>;
17
+ /** Appends a full or partial binding patch; `updatedAt` is bumped unless the patch sets it. */
18
+ export declare function patchDaemonSessionBinding(patch: Partial<Omit<DaemonSessionBinding, "externalKey">> & {
19
+ externalKey: string;
20
+ }): Promise<void>;