@parlor/agent-host 0.3.0 → 0.3.1

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.
@@ -9,6 +9,11 @@ export declare function optionChanges(options: readonly AgentOption[], chosen: R
9
9
  * CLI names under `unifiedWindows`, or the one window an event is about.
10
10
  */
11
11
  export declare function agentUsageOf(meta: Readonly<Record<string, unknown>> | null | undefined, at: number): AgentUsage | undefined;
12
+ /**
13
+ * Codex's rate limit, from `account/rateLimits/read` on its app-server: two windows it calls
14
+ * `primary` and `secondary`, each carrying its own length, so neither is taken to be the short one.
15
+ */
16
+ export declare function codexUsageOf(result: unknown, at: number): AgentUsage | undefined;
12
17
  /**
13
18
  * `next` laid over `held` window by window: an event names one window, and the others it
14
19
  * leaves out still stand until they roll over.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-options.d.ts","sourceRoot":"","sources":["../src/agent-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAoB,MAAM,cAAc,CAAC;AAE9E,qFAAqF;AACrF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,GACnD;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAQnD;AAiCD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAC1D,EAAE,EAAE,MAAM,GACT,UAAU,GAAG,SAAS,CAmBxB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAQrF"}
1
+ {"version":3,"file":"agent-options.d.ts","sourceRoot":"","sources":["../src/agent-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAoB,MAAM,cAAc,CAAC;AAE9E,qFAAqF;AACrF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,GACnD;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAQnD;AAiCD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAC1D,EAAE,EAAE,MAAM,GACT,UAAU,GAAG,SAAS,CAmBxB;AAQD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CA2BhF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAQrF"}
@@ -51,6 +51,36 @@ export function agentUsageOf(meta, at) {
51
51
  });
52
52
  return windows.length === 0 ? undefined : { windows: ordered(windows), at };
53
53
  }
54
+ /**
55
+ * Codex's rate limit, from `account/rateLimits/read` on its app-server: two windows it calls
56
+ * `primary` and `secondary`, each carrying its own length, so neither is taken to be the short one.
57
+ */
58
+ export function codexUsageOf(result, at) {
59
+ const limits = result?.rateLimits;
60
+ if (!limits || typeof limits !== "object")
61
+ return undefined;
62
+ const windows = ["primary", "secondary"].flatMap((id) => {
63
+ const slot = limits[id];
64
+ const used = slot?.usedPercent;
65
+ const minutes = slot?.windowDurationMins;
66
+ if (typeof used !== "number" || !Number.isFinite(used))
67
+ return [];
68
+ if (typeof minutes !== "number" || !(minutes > 0))
69
+ return [];
70
+ const resetsAt = typeof slot?.resetsAt === "number" && Number.isFinite(slot.resetsAt)
71
+ ? slot.resetsAt * 1000
72
+ : undefined;
73
+ return [
74
+ {
75
+ id,
76
+ minutes,
77
+ used: Math.min(1, Math.max(0, used / 100)),
78
+ ...(resetsAt === undefined ? {} : { resetsAt }),
79
+ },
80
+ ];
81
+ });
82
+ return windows.length === 0 ? undefined : { windows: ordered(windows), at };
83
+ }
54
84
  /**
55
85
  * `next` laid over `held` window by window: an event names one window, and the others it
56
86
  * leaves out still stand until they roll over.
@@ -69,5 +99,7 @@ function ordered(windows) {
69
99
  const ACCOUNT_WINDOWS = {
70
100
  five_hour: true,
71
101
  seven_day: true,
102
+ primary: true,
103
+ secondary: true,
72
104
  };
73
105
  //# sourceMappingURL=agent-options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-options.js","sourceRoot":"","sources":["../src/agent-options.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentOption, AgentUsage, AgentUsageWindow } from \"@parlor/chat\";\n\n/** The values `chosen` names that `options` offers and does not already run with. */\nexport function optionChanges(\n options: readonly AgentOption[],\n chosen: Readonly<Record<string, string>> | undefined,\n): { readonly id: string; readonly value: string }[] {\n return Object.entries(chosen ?? {}).flatMap(([id, value]) => {\n const option = options.find((candidate) => candidate.id === id);\n if (!option || option.current === value) return [];\n return option.values.some((candidate) => candidate.id === value)\n ? [{ id, value }]\n : [];\n });\n}\n\nconst RATE_LIMIT_META = \"_claude/rateLimit\";\nconst WINDOW_MINUTES: Readonly<Record<string, number>> = {\n five_hour: 5 * 60,\n seven_day: 7 * 24 * 60,\n seven_day_opus: 7 * 24 * 60,\n seven_day_sonnet: 7 * 24 * 60,\n};\n\ninterface RateLimitSlot {\n readonly utilization?: unknown;\n readonly resetsAt?: unknown;\n}\n\nfunction windowOf(id: string, slot: RateLimitSlot): AgentUsageWindow | undefined {\n const minutes = WINDOW_MINUTES[id];\n const used = slot.utilization;\n if (minutes === undefined || typeof used !== \"number\" || !Number.isFinite(used)) {\n return undefined;\n }\n const resetsAt =\n typeof slot.resetsAt === \"number\" && Number.isFinite(slot.resetsAt)\n ? slot.resetsAt * 1000\n : undefined;\n return {\n id,\n minutes,\n used: Math.min(1, Math.max(0, used)),\n ...(resetsAt === undefined ? {} : { resetsAt }),\n };\n}\n\n/**\n * Claude Code's rate limit, which rides on a `usage_update`'s `_meta`: every window a newer\n * CLI names under `unifiedWindows`, or the one window an event is about.\n */\nexport function agentUsageOf(\n meta: Readonly<Record<string, unknown>> | null | undefined,\n at: number,\n): AgentUsage | undefined {\n const info = meta?.[RATE_LIMIT_META] as\n | (RateLimitSlot & {\n readonly rateLimitType?: unknown;\n readonly unifiedWindows?: Readonly<Record<string, RateLimitSlot>>;\n })\n | undefined;\n if (!info || typeof info !== \"object\") return undefined;\n const slots: [string, RateLimitSlot][] =\n info.unifiedWindows && typeof info.unifiedWindows === \"object\"\n ? Object.entries(info.unifiedWindows)\n : typeof info.rateLimitType === \"string\"\n ? [[info.rateLimitType, info]]\n : [];\n const windows = slots.flatMap(([id, slot]) => {\n const window = windowOf(id, slot);\n return window ? [window] : [];\n });\n return windows.length === 0 ? undefined : { windows: ordered(windows), at };\n}\n\n/**\n * `next` laid over `held` window by window: an event names one window, and the others it\n * leaves out still stand until they roll over.\n */\nexport function mergeUsage(held: AgentUsage | undefined, next: AgentUsage): AgentUsage {\n const named = new Set(next.windows.map((window) => window.id));\n const kept = (held?.windows ?? []).filter(\n (window) =>\n !named.has(window.id) &&\n (window.resetsAt === undefined || window.resetsAt > next.at),\n );\n return { windows: ordered([...next.windows, ...kept]), at: next.at };\n}\n\n/** The account's own windows before a model's, each set shortest first. */\nfunction ordered(windows: readonly AgentUsageWindow[]): AgentUsageWindow[] {\n const perModel = (window: AgentUsageWindow) => (window.id in ACCOUNT_WINDOWS ? 0 : 1);\n return [...windows].sort(\n (a, b) =>\n perModel(a) - perModel(b) || a.minutes - b.minutes || a.id.localeCompare(b.id),\n );\n}\n\nconst ACCOUNT_WINDOWS: Readonly<Record<string, true>> = {\n five_hour: true,\n seven_day: true,\n};\n"]}
1
+ {"version":3,"file":"agent-options.js","sourceRoot":"","sources":["../src/agent-options.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentOption, AgentUsage, AgentUsageWindow } from \"@parlor/chat\";\n\n/** The values `chosen` names that `options` offers and does not already run with. */\nexport function optionChanges(\n options: readonly AgentOption[],\n chosen: Readonly<Record<string, string>> | undefined,\n): { readonly id: string; readonly value: string }[] {\n return Object.entries(chosen ?? {}).flatMap(([id, value]) => {\n const option = options.find((candidate) => candidate.id === id);\n if (!option || option.current === value) return [];\n return option.values.some((candidate) => candidate.id === value)\n ? [{ id, value }]\n : [];\n });\n}\n\nconst RATE_LIMIT_META = \"_claude/rateLimit\";\nconst WINDOW_MINUTES: Readonly<Record<string, number>> = {\n five_hour: 5 * 60,\n seven_day: 7 * 24 * 60,\n seven_day_opus: 7 * 24 * 60,\n seven_day_sonnet: 7 * 24 * 60,\n};\n\ninterface RateLimitSlot {\n readonly utilization?: unknown;\n readonly resetsAt?: unknown;\n}\n\nfunction windowOf(id: string, slot: RateLimitSlot): AgentUsageWindow | undefined {\n const minutes = WINDOW_MINUTES[id];\n const used = slot.utilization;\n if (minutes === undefined || typeof used !== \"number\" || !Number.isFinite(used)) {\n return undefined;\n }\n const resetsAt =\n typeof slot.resetsAt === \"number\" && Number.isFinite(slot.resetsAt)\n ? slot.resetsAt * 1000\n : undefined;\n return {\n id,\n minutes,\n used: Math.min(1, Math.max(0, used)),\n ...(resetsAt === undefined ? {} : { resetsAt }),\n };\n}\n\n/**\n * Claude Code's rate limit, which rides on a `usage_update`'s `_meta`: every window a newer\n * CLI names under `unifiedWindows`, or the one window an event is about.\n */\nexport function agentUsageOf(\n meta: Readonly<Record<string, unknown>> | null | undefined,\n at: number,\n): AgentUsage | undefined {\n const info = meta?.[RATE_LIMIT_META] as\n | (RateLimitSlot & {\n readonly rateLimitType?: unknown;\n readonly unifiedWindows?: Readonly<Record<string, RateLimitSlot>>;\n })\n | undefined;\n if (!info || typeof info !== \"object\") return undefined;\n const slots: [string, RateLimitSlot][] =\n info.unifiedWindows && typeof info.unifiedWindows === \"object\"\n ? Object.entries(info.unifiedWindows)\n : typeof info.rateLimitType === \"string\"\n ? [[info.rateLimitType, info]]\n : [];\n const windows = slots.flatMap(([id, slot]) => {\n const window = windowOf(id, slot);\n return window ? [window] : [];\n });\n return windows.length === 0 ? undefined : { windows: ordered(windows), at };\n}\n\ninterface CodexWindow {\n readonly usedPercent?: unknown;\n readonly windowDurationMins?: unknown;\n readonly resetsAt?: unknown;\n}\n\n/**\n * Codex's rate limit, from `account/rateLimits/read` on its app-server: two windows it calls\n * `primary` and `secondary`, each carrying its own length, so neither is taken to be the short one.\n */\nexport function codexUsageOf(result: unknown, at: number): AgentUsage | undefined {\n const limits = (\n result as { readonly rateLimits?: Readonly<Record<string, CodexWindow>> }\n )?.rateLimits;\n if (!limits || typeof limits !== \"object\") return undefined;\n const windows = ([\"primary\", \"secondary\"] as const).flatMap(\n (id): AgentUsageWindow[] => {\n const slot = limits[id];\n const used = slot?.usedPercent;\n const minutes = slot?.windowDurationMins;\n if (typeof used !== \"number\" || !Number.isFinite(used)) return [];\n if (typeof minutes !== \"number\" || !(minutes > 0)) return [];\n const resetsAt =\n typeof slot?.resetsAt === \"number\" && Number.isFinite(slot.resetsAt)\n ? slot.resetsAt * 1000\n : undefined;\n return [\n {\n id,\n minutes,\n used: Math.min(1, Math.max(0, used / 100)),\n ...(resetsAt === undefined ? {} : { resetsAt }),\n },\n ];\n },\n );\n return windows.length === 0 ? undefined : { windows: ordered(windows), at };\n}\n\n/**\n * `next` laid over `held` window by window: an event names one window, and the others it\n * leaves out still stand until they roll over.\n */\nexport function mergeUsage(held: AgentUsage | undefined, next: AgentUsage): AgentUsage {\n const named = new Set(next.windows.map((window) => window.id));\n const kept = (held?.windows ?? []).filter(\n (window) =>\n !named.has(window.id) &&\n (window.resetsAt === undefined || window.resetsAt > next.at),\n );\n return { windows: ordered([...next.windows, ...kept]), at: next.at };\n}\n\n/** The account's own windows before a model's, each set shortest first. */\nfunction ordered(windows: readonly AgentUsageWindow[]): AgentUsageWindow[] {\n const perModel = (window: AgentUsageWindow) => (window.id in ACCOUNT_WINDOWS ? 0 : 1);\n return [...windows].sort(\n (a, b) =>\n perModel(a) - perModel(b) || a.minutes - b.minutes || a.id.localeCompare(b.id),\n );\n}\n\nconst ACCOUNT_WINDOWS: Readonly<Record<string, true>> = {\n five_hour: true,\n seven_day: true,\n primary: true,\n secondary: true,\n};\n"]}
@@ -0,0 +1,15 @@
1
+ import type { AgentOption, AgentUsage } from "@parlor/chat";
2
+ import type { AgentLauncher } from "./acp.js";
3
+ import type { AgentProfile } from "./agents.js";
4
+ /**
5
+ * The options `profile` offers, from a session opened in a scratch directory only to read
6
+ * them and then closed: no prompt, so no model turn. Lets the sidebar offer a model before
7
+ * the agent's first turn. Undefined when the agent cannot be started or is not logged in.
8
+ */
9
+ export declare function surveyOptions(profile: AgentProfile, launcher: AgentLauncher, env: Readonly<Record<string, string>>, stop?: AbortSignal): Promise<readonly AgentOption[] | undefined>;
10
+ /**
11
+ * The account's allowance from the agent's own CLI, for an agent whose ACP adapter does not
12
+ * carry it: `codex-acp` drops Codex's rate limits. Claude Code sends its own over ACP.
13
+ */
14
+ export declare function readUsage(profile: AgentProfile, env: Readonly<Record<string, string>>, now: number): Promise<AgentUsage | undefined>;
15
+ //# sourceMappingURL=agent-survey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-survey.d.ts","sourceRoot":"","sources":["../src/agent-survey.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACrC,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,SAAS,WAAW,EAAE,GAAG,SAAS,CAAC,CAyB7C;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACrC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAMjC"}
@@ -0,0 +1,51 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { codexUsageOf } from "./agent-options.js";
5
+ import { askCodex } from "./codex-app-server.js";
6
+ const SURVEY_TIMEOUT_MS = 60_000;
7
+ /**
8
+ * The options `profile` offers, from a session opened in a scratch directory only to read
9
+ * them and then closed: no prompt, so no model turn. Lets the sidebar offer a model before
10
+ * the agent's first turn. Undefined when the agent cannot be started or is not logged in.
11
+ */
12
+ export async function surveyOptions(profile, launcher, env, stop) {
13
+ const cwd = await mkdtemp(join(tmpdir(), "parlor-survey-"));
14
+ const timeout = AbortSignal.timeout(SURVEY_TIMEOUT_MS);
15
+ const signal = stop ? AbortSignal.any([stop, timeout]) : timeout;
16
+ try {
17
+ const connection = await launcher.launch(profile, { cwd, env, signal });
18
+ const end = () => void connection.kill();
19
+ signal.addEventListener("abort", end, { once: true });
20
+ try {
21
+ if (!connection.capabilities.ready)
22
+ return undefined;
23
+ const session = await connection.openSession({
24
+ cwd,
25
+ mcpServers: [],
26
+ pinAskMode: true,
27
+ });
28
+ return session.options;
29
+ }
30
+ finally {
31
+ signal.removeEventListener("abort", end);
32
+ await connection.kill();
33
+ }
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ finally {
39
+ await rm(cwd, { recursive: true, force: true });
40
+ }
41
+ }
42
+ /**
43
+ * The account's allowance from the agent's own CLI, for an agent whose ACP adapter does not
44
+ * carry it: `codex-acp` drops Codex's rate limits. Claude Code sends its own over ACP.
45
+ */
46
+ export async function readUsage(profile, env, now) {
47
+ if (profile.id !== "codex")
48
+ return undefined;
49
+ return codexUsageOf(await askCodex(profile.binary, "account/rateLimits/read", null, env), now);
50
+ }
51
+ //# sourceMappingURL=agent-survey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-survey.js","sourceRoot":"","sources":["../src/agent-survey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAKjC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAEjC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAqB,EACrB,QAAuB,EACvB,GAAqC,EACrC,IAAkB;IAElB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACjE,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QACxE,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC,KAAK,UAAU,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;YACrD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC;gBAC3C,GAAG;gBACH,UAAU,EAAE,EAAE;gBACd,UAAU,EAAE,IAAI;aACjB,CAAC,CAAC;YACH,OAAO,OAAO,CAAC,OAAO,CAAC;QACzB,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACzC,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAqB,EACrB,GAAqC,EACrC,GAAW;IAEX,IAAI,OAAO,CAAC,EAAE,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IAC7C,OAAO,YAAY,CACjB,MAAM,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,yBAAyB,EAAE,IAAI,EAAE,GAAG,CAAC,EACpE,GAAG,CACJ,CAAC;AACJ,CAAC","sourcesContent":["import { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport type { AgentOption, AgentUsage } from \"@parlor/chat\";\n\nimport type { AgentLauncher } from \"./acp.js\";\nimport { codexUsageOf } from \"./agent-options.js\";\nimport type { AgentProfile } from \"./agents.js\";\nimport { askCodex } from \"./codex-app-server.js\";\n\nconst SURVEY_TIMEOUT_MS = 60_000;\n\n/**\n * The options `profile` offers, from a session opened in a scratch directory only to read\n * them and then closed: no prompt, so no model turn. Lets the sidebar offer a model before\n * the agent's first turn. Undefined when the agent cannot be started or is not logged in.\n */\nexport async function surveyOptions(\n profile: AgentProfile,\n launcher: AgentLauncher,\n env: Readonly<Record<string, string>>,\n stop?: AbortSignal,\n): Promise<readonly AgentOption[] | undefined> {\n const cwd = await mkdtemp(join(tmpdir(), \"parlor-survey-\"));\n const timeout = AbortSignal.timeout(SURVEY_TIMEOUT_MS);\n const signal = stop ? AbortSignal.any([stop, timeout]) : timeout;\n try {\n const connection = await launcher.launch(profile, { cwd, env, signal });\n const end = () => void connection.kill();\n signal.addEventListener(\"abort\", end, { once: true });\n try {\n if (!connection.capabilities.ready) return undefined;\n const session = await connection.openSession({\n cwd,\n mcpServers: [],\n pinAskMode: true,\n });\n return session.options;\n } finally {\n signal.removeEventListener(\"abort\", end);\n await connection.kill();\n }\n } catch {\n return undefined;\n } finally {\n await rm(cwd, { recursive: true, force: true });\n }\n}\n\n/**\n * The account's allowance from the agent's own CLI, for an agent whose ACP adapter does not\n * carry it: `codex-acp` drops Codex's rate limits. Claude Code sends its own over ACP.\n */\nexport async function readUsage(\n profile: AgentProfile,\n env: Readonly<Record<string, string>>,\n now: number,\n): Promise<AgentUsage | undefined> {\n if (profile.id !== \"codex\") return undefined;\n return codexUsageOf(\n await askCodex(profile.binary, \"account/rateLimits/read\", null, env),\n now,\n );\n}\n"]}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * One JSON-RPC call to a Codex app-server of its own, started for it and stopped after. For
3
+ * what Codex keeps but `codex-acp` drops, such as the account's rate limits; it costs no
4
+ * model turn. Undefined when anything goes wrong: Codex is missing, slow, or answers oddly.
5
+ */
6
+ export declare function askCodex(binary: string, method: string, params: unknown, env: Readonly<Record<string, string>>): Promise<unknown>;
7
+ //# sourceMappingURL=codex-app-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-app-server.d.ts","sourceRoot":"","sources":["../src/codex-app-server.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,OAAO,CAAC,OAAO,CAAC,CA+BlB"}
@@ -0,0 +1,46 @@
1
+ import { spawn } from "node:child_process";
2
+ import { onLines } from "./lines.js";
3
+ const INITIALIZE = 1;
4
+ const CALL = 2;
5
+ const TIMEOUT_MS = 30_000;
6
+ /** Codex only echoes it back inside a user agent. */
7
+ const CLIENT_INFO = { name: "parlor-host", version: "1" };
8
+ /**
9
+ * One JSON-RPC call to a Codex app-server of its own, started for it and stopped after. For
10
+ * what Codex keeps but `codex-acp` drops, such as the account's rate limits; it costs no
11
+ * model turn. Undefined when anything goes wrong: Codex is missing, slow, or answers oddly.
12
+ */
13
+ export function askCodex(binary, method, params, env) {
14
+ return new Promise((resolve) => {
15
+ let answer;
16
+ const child = spawn(binary, ["app-server"], {
17
+ env,
18
+ stdio: ["pipe", "pipe", "ignore"],
19
+ });
20
+ const timer = setTimeout(() => child.kill("SIGKILL"), TIMEOUT_MS);
21
+ const send = (message) => child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
22
+ onLines(child.stdout, (line) => {
23
+ let message;
24
+ try {
25
+ message = JSON.parse(line);
26
+ }
27
+ catch {
28
+ return;
29
+ }
30
+ if (message.id === INITIALIZE)
31
+ send({ id: CALL, method, params });
32
+ else if (message.id === CALL) {
33
+ answer = message.result;
34
+ child.kill();
35
+ }
36
+ });
37
+ child.stdin.on("error", () => undefined);
38
+ child.once("error", () => resolve(undefined));
39
+ child.once("close", () => {
40
+ clearTimeout(timer);
41
+ resolve(answer);
42
+ });
43
+ send({ id: INITIALIZE, method: "initialize", params: { clientInfo: CLIENT_INFO } });
44
+ });
45
+ }
46
+ //# sourceMappingURL=codex-app-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-app-server.js","sourceRoot":"","sources":["../src/codex-app-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,IAAI,GAAG,CAAC,CAAC;AACf,MAAM,UAAU,GAAG,MAAM,CAAC;AAC1B,qDAAqD;AACrD,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAE1D;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,MAAc,EACd,MAAc,EACd,MAAe,EACf,GAAqC;IAErC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,MAAe,CAAC;QACpB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,YAAY,CAAC,EAAE;YAC1C,GAAG;YACH,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;SAClC,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,CAAC;QAClE,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE,CAC/B,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3E,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YAC7B,IAAI,OAA6D,CAAC;YAClE,IAAI,CAAC;gBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAC;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO;YACT,CAAC;YACD,IAAI,OAAO,CAAC,EAAE,KAAK,UAAU;gBAAE,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;iBAC7D,IAAI,OAAO,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;gBAC7B,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBACxB,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,CAAC;QACH,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACzC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;YACvB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { spawn } from \"node:child_process\";\n\nimport { onLines } from \"./lines.js\";\n\nconst INITIALIZE = 1;\nconst CALL = 2;\nconst TIMEOUT_MS = 30_000;\n/** Codex only echoes it back inside a user agent. */\nconst CLIENT_INFO = { name: \"parlor-host\", version: \"1\" };\n\n/**\n * One JSON-RPC call to a Codex app-server of its own, started for it and stopped after. For\n * what Codex keeps but `codex-acp` drops, such as the account's rate limits; it costs no\n * model turn. Undefined when anything goes wrong: Codex is missing, slow, or answers oddly.\n */\nexport function askCodex(\n binary: string,\n method: string,\n params: unknown,\n env: Readonly<Record<string, string>>,\n): Promise<unknown> {\n return new Promise((resolve) => {\n let answer: unknown;\n const child = spawn(binary, [\"app-server\"], {\n env,\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n const timer = setTimeout(() => child.kill(\"SIGKILL\"), TIMEOUT_MS);\n const send = (message: object) =>\n child.stdin.write(`${JSON.stringify({ jsonrpc: \"2.0\", ...message })}\\n`);\n onLines(child.stdout, (line) => {\n let message: { readonly id?: unknown; readonly result?: unknown };\n try {\n message = JSON.parse(line) as typeof message;\n } catch {\n return;\n }\n if (message.id === INITIALIZE) send({ id: CALL, method, params });\n else if (message.id === CALL) {\n answer = message.result;\n child.kill();\n }\n });\n child.stdin.on(\"error\", () => undefined);\n child.once(\"error\", () => resolve(undefined));\n child.once(\"close\", () => {\n clearTimeout(timer);\n resolve(answer);\n });\n send({ id: INITIALIZE, method: \"initialize\", params: { clientInfo: CLIENT_INFO } });\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAU9C,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,eAAe,CAAC;AAI9D,OAAO,EAAE,KAAK,WAAW,EAAoC,MAAM,cAAc,CAAC;AAElF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,kFAAkF;IAClF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnE,kFAAkF;IAClF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChF,4EAA4E;IAC5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAiBD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,GAAG,SAAuB,GACtC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAWD,6FAA6F;AAC7F,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAkB,IAAI,wBAAwB;IAC9C,cAEC;CACF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CAsoBhE"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAW9C,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,eAAe,CAAC;AAI9D,OAAO,EAAE,KAAK,WAAW,EAAoC,MAAM,cAAc,CAAC;AAElF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,kFAAkF;IAClF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnE,kFAAkF;IAClF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChF,4EAA4E;IAC5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAiBD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,GAAG,SAAuB,GACtC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAWD,6FAA6F;AAC7F,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAkB,IAAI,wBAAwB;IAC9C,cAEC;CACF;AAKD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CAssBhE"}
package/dist/daemon.js CHANGED
@@ -3,6 +3,7 @@ import { createServer } from "node:net";
3
3
  import { basename, delimiter, dirname, join } from "node:path";
4
4
  import { HOST_PROTOCOL, } from "@parlor/chat";
5
5
  import { ActionError, asId, DEV_USER_ID } from "@parlor/core";
6
+ import { readUsage, surveyOptions } from "./agent-survey.js";
6
7
  import { agentNames, registerAgentTools, unregisterAgentTools, } from "./agent-tools.js";
7
8
  import { detectAgents, loadProfiles } from "./agents.js";
8
9
  import { connectControl } from "./control.js";
@@ -63,6 +64,8 @@ export class HostAlreadyRunning extends Error {
63
64
  super("Parlor Host is already running on this computer.");
64
65
  }
65
66
  }
67
+ /** Codex's own CLI throttles its rate-limit reports on the same interval. */
68
+ const USAGE_READ_FLOOR_MS = 30_000;
66
69
  export function createHostDaemon(deps) {
67
70
  const { paths } = deps;
68
71
  const links = new Map();
@@ -89,14 +92,75 @@ export function createHostDaemon(deps) {
89
92
  return undefined;
90
93
  }
91
94
  };
95
+ /** What an agent said about itself, told to every linked app when it changes what they show. */
96
+ const learn = (agentId, news) => {
97
+ void learned
98
+ .learn(agentId, news)
99
+ .then((changed) => {
100
+ if (!changed)
101
+ return;
102
+ for (const linked of links.values())
103
+ void report(linked).catch(() => undefined);
104
+ })
105
+ .catch(() => undefined);
106
+ };
107
+ /** Ends the reads below on stop or handover, so nothing outlives the daemon. */
108
+ const stopping = new AbortController();
109
+ const usageReadAt = new Map();
110
+ /** The allowance from the agent's own CLI where ACP carries none, at most every 30s per agent. */
111
+ const refreshUsage = async (agentId) => {
112
+ const profile = profiles.find((candidate) => candidate.id === agentId);
113
+ const now = Date.now();
114
+ if (!profile || stopping.signal.aborted)
115
+ return;
116
+ if (now - (usageReadAt.get(agentId) ?? 0) < USAGE_READ_FLOOR_MS)
117
+ return;
118
+ usageReadAt.set(agentId, now);
119
+ const usage = await readUsage(profile, await agentEnvironment(), now).catch(() => undefined);
120
+ if (usage && !stopping.signal.aborted)
121
+ learn(agentId, { usage });
122
+ };
123
+ let surveyed = false;
124
+ /**
125
+ * Options for every agent that has not run a turn yet, so the sidebar can offer a model first.
126
+ * Once per run, when an app first links: a computer no app uses never starts its agents.
127
+ */
128
+ const surveyAgents = async () => {
129
+ if (surveyed)
130
+ return;
131
+ surveyed = true;
132
+ const env = await agentEnvironment();
133
+ for (const agent of agents) {
134
+ const profile = profiles.find((candidate) => candidate.id === agent.id);
135
+ if (!profile || stopping.signal.aborted)
136
+ continue;
137
+ void refreshUsage(agent.id);
138
+ if (learned.knows(agent.id))
139
+ continue;
140
+ const options = await surveyOptions(profile, deps.launcher, env, stopping.signal);
141
+ if (stopping.signal.aborted)
142
+ return;
143
+ logEvent("agent_surveyed", { agentId: agent.id, options: options?.length ?? 0 });
144
+ if (options && options.length > 0)
145
+ learn(agent.id, { options });
146
+ }
147
+ };
92
148
  const running = new Set();
93
149
  let handedOver = false;
94
150
  let finishing = false;
95
151
  let finishHandover = () => undefined;
96
152
  const drained = new Promise((resolve) => (finishHandover = resolve));
97
- const run = (state, claimed) => {
153
+ /** A turn runs once here, even when a pushed turn its app took back is claimed again. */
154
+ const runningTurns = new Set();
155
+ const run = (state, claimed, how = "claimed") => {
98
156
  const { link } = state;
99
- logEvent("turn_claimed", { appOrigin: link.grant.appOrigin, turnId: claimed.turnId });
157
+ if (runningTurns.has(claimed.turnId))
158
+ return;
159
+ runningTurns.add(claimed.turnId);
160
+ logEvent(how === "pushed" ? "turn_pushed" : "turn_claimed", {
161
+ appOrigin: link.grant.appOrigin,
162
+ turnId: claimed.turnId,
163
+ });
100
164
  state.turns += 1;
101
165
  const turn = (link.grant.dev ? Promise.resolve(DEV_POLICY) : loadPolicy(paths.policy, deps.policy))
102
166
  .then((policy) => runTurn(claimed, {
@@ -119,17 +183,7 @@ export function createHostDaemon(deps) {
119
183
  policy,
120
184
  prompter: deps.prompter,
121
185
  now: Date.now,
122
- learned: (agentId, news) => {
123
- void learned
124
- .learn(agentId, news)
125
- .then((changed) => {
126
- if (!changed)
127
- return;
128
- for (const linked of links.values())
129
- void report(linked).catch(() => undefined);
130
- })
131
- .catch(() => undefined);
132
- },
186
+ learned: learn,
133
187
  pairing: {
134
188
  secret: async () => (await deps.grants.get(link.grant.appOrigin))?.pairingSecret,
135
189
  forget: async () => {
@@ -143,6 +197,8 @@ export function createHostDaemon(deps) {
143
197
  }))
144
198
  .then((outcome) => logEvent("turn_finished", { turnId: claimed.turnId, outcome }), (error) => logEvent("turn_finished", { turnId: claimed.turnId, error: String(error) }))
145
199
  .finally(() => {
200
+ runningTurns.delete(claimed.turnId);
201
+ void refreshUsage(claimed.agent.agentId);
146
202
  state.turns -= 1;
147
203
  running.delete(turn);
148
204
  settleHandover();
@@ -256,6 +312,7 @@ export function createHostDaemon(deps) {
256
312
  await deps.pool.forget(grant);
257
313
  };
258
314
  const open = (grant, keys) => {
315
+ void surveyAgents().catch(() => undefined);
259
316
  const origin = grant.appOrigin;
260
317
  latest.set(origin, grant);
261
318
  let state;
@@ -266,6 +323,13 @@ export function createHostDaemon(deps) {
266
323
  platform,
267
324
  fetch: (input, init) => fetch(input, init),
268
325
  onWake: () => wake(state),
326
+ onPushed: (_link, turn) => {
327
+ // Tells the app this computer has the turn, while it already starts on it.
328
+ void link.api.calls
329
+ .call("hostAppend", { turnId: turn.turnId, items: [] })
330
+ .catch(() => undefined);
331
+ run(state, turn, "pushed");
332
+ },
269
333
  busy: () => state.turns > 0,
270
334
  onRevoked: () => {
271
335
  if (links.get(origin) !== state)
@@ -485,6 +549,7 @@ export function createHostDaemon(deps) {
485
549
  if (handedOver)
486
550
  return;
487
551
  handedOver = true;
552
+ stopping.abort();
488
553
  for (const state of links.values())
489
554
  state.link.drain();
490
555
  await Promise.all([...links.values()].map((state) => state.claiming));
@@ -495,6 +560,7 @@ export function createHostDaemon(deps) {
495
560
  logEvent("handover_started", { version: deps.release.version });
496
561
  },
497
562
  async stop() {
563
+ stopping.abort();
498
564
  if (!handedOver) {
499
565
  await Promise.all([...links.values()].map((state) => Promise.race([
500
566
  state.link.api.calls.call("hostReport", {