@pellux/goodvibes-daemon 1.28.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 (113) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/bin/goodvibes-daemon +100 -0
  5. package/bin/launcher-support.js +226 -0
  6. package/package.json +96 -0
  7. package/scripts/check-bun.sh +20 -0
  8. package/scripts/postinstall.js +244 -0
  9. package/src/cli/command-catalog.ts +828 -0
  10. package/src/cli/completion.ts +299 -0
  11. package/src/cli/help.ts +167 -0
  12. package/src/cli/index.ts +21 -0
  13. package/src/cli/parser.ts +55 -0
  14. package/src/cli/surface-catalog.ts +26 -0
  15. package/src/cli/types.ts +63 -0
  16. package/src/cluster/daemon-ws-call.ts +235 -0
  17. package/src/cluster/raw-reply-route.ts +111 -0
  18. package/src/config/checkpoint-settings.ts +113 -0
  19. package/src/config/run-daemon-config-migration.ts +47 -0
  20. package/src/config/secret-config.ts +175 -0
  21. package/src/config/secrets.ts +71 -0
  22. package/src/config/surface.ts +24 -0
  23. package/src/core/pairing-banner.ts +82 -0
  24. package/src/daemon/cli.ts +878 -0
  25. package/src/daemon/config-command.ts +281 -0
  26. package/src/daemon/handlers/context.ts +29 -0
  27. package/src/daemon/handlers/contracts.ts +43 -0
  28. package/src/daemon/handlers/credentials.ts +139 -0
  29. package/src/daemon/handlers/drafts/draft-store.ts +427 -0
  30. package/src/daemon/handlers/drafts/index.ts +17 -0
  31. package/src/daemon/handlers/drafts/register.ts +331 -0
  32. package/src/daemon/handlers/errors.ts +18 -0
  33. package/src/daemon/handlers/inbox/aggregator.ts +375 -0
  34. package/src/daemon/handlers/inbox/cursor-store.ts +512 -0
  35. package/src/daemon/handlers/inbox/index.ts +221 -0
  36. package/src/daemon/handlers/inbox/mapping.ts +192 -0
  37. package/src/daemon/handlers/inbox/poller.ts +239 -0
  38. package/src/daemon/handlers/inbox/provider-adapter.ts +171 -0
  39. package/src/daemon/handlers/inbox/providers/discord.ts +276 -0
  40. package/src/daemon/handlers/inbox/providers/email.ts +176 -0
  41. package/src/daemon/handlers/inbox/providers/imap-client.ts +300 -0
  42. package/src/daemon/handlers/inbox/providers/route-util.ts +24 -0
  43. package/src/daemon/handlers/inbox/providers/slack.ts +287 -0
  44. package/src/daemon/handlers/index.ts +117 -0
  45. package/src/daemon/handlers/register.ts +180 -0
  46. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +143 -0
  47. package/src/daemon/handlers/remote/backends/docker.ts +79 -0
  48. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  49. package/src/daemon/handlers/remote/backends/local-process.ts +113 -0
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +127 -0
  51. package/src/daemon/handlers/remote/backends/ssh.ts +126 -0
  52. package/src/daemon/handlers/remote/backends/types.ts +97 -0
  53. package/src/daemon/handlers/remote/dispatcher.ts +181 -0
  54. package/src/daemon/handlers/remote/index.ts +120 -0
  55. package/src/daemon/handlers/remote/peer-registry.ts +357 -0
  56. package/src/daemon/handlers/remote/service.ts +191 -0
  57. package/src/daemon/handlers/routing/inbox-bridge.ts +71 -0
  58. package/src/daemon/handlers/routing/index.ts +261 -0
  59. package/src/daemon/handlers/routing/route-store.ts +319 -0
  60. package/src/daemon/handlers/routing/routing-resolver.ts +75 -0
  61. package/src/daemon/handlers/sqlite-store.ts +303 -0
  62. package/src/daemon/handlers/triage/index.ts +57 -0
  63. package/src/daemon/handlers/triage/integration.ts +213 -0
  64. package/src/daemon/handlers/triage/pipeline.ts +274 -0
  65. package/src/daemon/handlers/triage/scorer.ts +287 -0
  66. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  67. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  68. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  69. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  70. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  71. package/src/daemon/handlers/triage/types.ts +50 -0
  72. package/src/daemon/lifecycle.ts +41 -0
  73. package/src/daemon/local-daemon-state.ts +233 -0
  74. package/src/daemon/pair-command.ts +301 -0
  75. package/src/daemon/provision-wake-model.ts +81 -0
  76. package/src/daemon/send/channels.ts +200 -0
  77. package/src/daemon/send/command.ts +333 -0
  78. package/src/daemon/send/composition.ts +100 -0
  79. package/src/daemon/send/failure-text.ts +93 -0
  80. package/src/daemon/send/inert-text.ts +225 -0
  81. package/src/daemon/send/stdin.ts +24 -0
  82. package/src/daemon/service-commands.ts +530 -0
  83. package/src/daemon/sessions-command.ts +209 -0
  84. package/src/daemon/status-command.ts +481 -0
  85. package/src/daemon/webui-command.ts +339 -0
  86. package/src/runtime/boot-tasks.ts +110 -0
  87. package/src/runtime/cluster-composition.ts +124 -0
  88. package/src/runtime/cluster-group-composition.ts +284 -0
  89. package/src/runtime/conversation-rewind-port.ts +171 -0
  90. package/src/runtime/credential-composition.ts +54 -0
  91. package/src/runtime/daemon-handler-composition.ts +76 -0
  92. package/src/runtime/device-posture-composition.ts +115 -0
  93. package/src/runtime/disposal-wiring.ts +101 -0
  94. package/src/runtime/fleet-needs-input-push.ts +61 -0
  95. package/src/runtime/fleet-services.ts +41 -0
  96. package/src/runtime/hosted-session-composition.ts +128 -0
  97. package/src/runtime/index.ts +100 -0
  98. package/src/runtime/knowledge-services.ts +101 -0
  99. package/src/runtime/legacy-daemon-migration.ts +605 -0
  100. package/src/runtime/legacy-daemon-reconcile.ts +448 -0
  101. package/src/runtime/mail-composition.ts +65 -0
  102. package/src/runtime/notification-dispatch.ts +86 -0
  103. package/src/runtime/plugin-composition.ts +111 -0
  104. package/src/runtime/runtime-services-types.ts +268 -0
  105. package/src/runtime/services.ts +756 -0
  106. package/src/runtime/trigger-services.ts +62 -0
  107. package/src/runtime/trust/checkpoint-eligibility.ts +138 -0
  108. package/src/runtime/trust/trust-gated-approvals.ts +169 -0
  109. package/src/runtime/update-check.ts +61 -0
  110. package/src/runtime/workspace-checkpointing.ts +116 -0
  111. package/src/testing/daemon-fixture.ts +276 -0
  112. package/src/testing/hosted-session-failures.ts +92 -0
  113. package/src/version.ts +26 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * daemon-fixture.ts — a real, running daemon, composed the way this product
3
+ * composes one, for a test to drive.
4
+ *
5
+ * ── Why this is a shipped module and not a test helper ────────────────────
6
+ *
7
+ * Three consumer repositories test their client against the daemon's contract,
8
+ * and each of them hand-builds a composition to test against: a stub session
9
+ * broker here, a fake approval broker there, a catalog with the handlers
10
+ * somebody remembered to attach. They are large (several hundred lines each),
11
+ * they duplicate each other, and — the part that matters — they are each a
12
+ * SECOND idea of what a daemon is. A client contract test passing against a
13
+ * hand-rolled stand-in tells you the stand-in agrees with the client, which is
14
+ * the one agreement that was never in doubt.
15
+ *
16
+ * This module is the alternative: the daemon's own `createRuntimeServices` and
17
+ * its own `DaemonServer`, bound on an ephemeral port with a known token, over a
18
+ * temp home. What a consumer's test talks to is the thing that ships.
19
+ *
20
+ * It deliberately lives under `src/testing/` rather than `src/test/`: the
21
+ * package's `files` list ships `src` and excludes `src/test`, so this path is
22
+ * published while the suites are not, and the main tsconfig typechecks it as
23
+ * ordinary source. Nothing here imports `bun:test`, so a consumer on any runner
24
+ * can use it.
25
+ *
26
+ * ── What it is not ───────────────────────────────────────────────────────
27
+ *
28
+ * Not a mock and not a fixture file. Nothing here fabricates a response. If a
29
+ * verb is unwired in this composition it answers exactly as unwired as it would
30
+ * in production, which is the property a contract test needs and a hand-built
31
+ * stand-in cannot have.
32
+ *
33
+ * ── What a consumer needs, and what is still missing ─────────────────────
34
+ *
35
+ * WORKS TODAY, from inside this package, with no change anywhere else:
36
+ *
37
+ * import { startDaemonFixture } from 'goodvibes-daemon/src/testing/daemon-fixture.ts';
38
+ *
39
+ * The package declares no `exports` map, so a deep path resolves. It is the
40
+ * ugly form of the import, and it is a real one.
41
+ *
42
+ * STILL MISSING, and each is somebody's decision rather than a thing this file
43
+ * can do for itself:
44
+ *
45
+ * 1. A NAMED ENTRY POINT. `goodvibes-daemon/testing` instead of the deep
46
+ * path. That means adding an `exports` map to package.json — and adding
47
+ * one is not additive: an `exports` map REPLACES path-based resolution, so
48
+ * every existing deep import into this package (its own `bin` shim
49
+ * included) stops resolving unless the map enumerates them. That is a
50
+ * packaging change with a blast radius, made once, deliberately, by
51
+ * whoever owns distribution — not a side effect of adding a test helper.
52
+ *
53
+ * 2. THE CONSUMER DEPENDENCY. A consumer repo has to depend on
54
+ * `goodvibes-daemon` to import this at all. Today the agent does (it is
55
+ * what makes its own install fail against an unpublished version); the
56
+ * terminal app and the webui do not. For the terminal app that is a
57
+ * devDependency and a version pin. For the webui, whose suites run in a
58
+ * browser context, the fixture cannot run in-process at all — it needs a
59
+ * launcher script that starts the fixture in a node process and hands the
60
+ * Playwright suite `baseUrl` and `token`. That launcher does not exist and
61
+ * should be written on the webui side, where its runner lives.
62
+ *
63
+ * 3. A RUNTIME FLOOR. This composes real stores, opens a real socket, and
64
+ * starts the runtime graph's pollers. A consumer suite that spins one up
65
+ * per test file pays about a second each and must `stop()` every one; the
66
+ * three hand-built stand-ins it replaces cost nothing and leak nothing.
67
+ * The honest guidance is one fixture per FILE (`beforeAll`/`afterAll`),
68
+ * which is how this repository's own suites use it — not one per test.
69
+ *
70
+ * 4. WHAT IT DOES NOT REPLACE. A stand-in is still the right tool for
71
+ * driving a client through a daemon state that is hard to reach for real:
72
+ * a wedged session, a specific 500, a torn connection. This fixture
73
+ * replaces the stand-ins that exist only to answer normally — which is
74
+ * most of the several hundred lines in each consumer, and all of the part
75
+ * that silently drifts. The refusal shapes it cannot easily produce live
76
+ * are exported separately and pinned against the real engine; see
77
+ * ./hosted-session-failures.ts for the pattern.
78
+ */
79
+ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
80
+ import { tmpdir } from 'node:os';
81
+ import { join } from 'node:path';
82
+ import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
83
+ import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon';
84
+ import { createRuntimeStore } from '@pellux/goodvibes-sdk/platform/runtime/store';
85
+ import { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
86
+ import { createFeatureFlagManager, deriveFeatureStates, RuntimeEventBus } from '@/runtime/index.ts';
87
+ import { createRuntimeServices, type RuntimeServices } from '../runtime/services.ts';
88
+ import { createHostedSessionOptions } from '../runtime/hosted-session-composition.ts';
89
+
90
+ /**
91
+ * The capabilities a daemon serves once it is running. Seeded from a real
92
+ * config first (so the registry stays complete), then forced on, because a
93
+ * contract test asking "does this daemon serve X" must not be answered by a
94
+ * feature flag that happens to be off in a fresh temp home.
95
+ */
96
+ const DAEMON_CAPABILITY_FLAGS: readonly string[] = [
97
+ 'automation-domain',
98
+ 'control-plane-gateway',
99
+ 'delivery-engine',
100
+ 'hitl-ux-modes',
101
+ 'ntfy-surface',
102
+ 'permission-divergence-dashboard',
103
+ 'policy-as-code',
104
+ 'route-binding',
105
+ 'service-management',
106
+ 'slack-surface',
107
+ 'unified-runtime-task',
108
+ 'watcher-framework',
109
+ 'web-surface',
110
+ 'webhook-surface',
111
+ ];
112
+
113
+ export interface DaemonFixtureOptions {
114
+ /**
115
+ * Root directory for this fixture's home and workspace. Omitted ⇒ a fresh
116
+ * `mkdtemp` under the OS temp directory, removed by `stop()`. Pass one when
117
+ * the caller has its own temp-tree bookkeeping (this repo's suites do).
118
+ */
119
+ readonly root?: string;
120
+ /** Bearer token the daemon accepts. Omitted ⇒ a generated per-fixture token. */
121
+ readonly token?: string;
122
+ /** Last-chance hook to write config before the runtime graph is built. */
123
+ readonly configure?: (configManager: ConfigManager) => void;
124
+ /**
125
+ * Feature flag ids to force on, replacing the default daemon capability set.
126
+ * Rarely needed; stated so a caller testing a flag's OFF behaviour can.
127
+ */
128
+ readonly featureFlagIds?: readonly string[];
129
+ /**
130
+ * Whether this daemon hosts sessions of its own, the way the real entrypoint
131
+ * states it (`src/daemon/cli.ts` passes `createHostedSessionOptions`).
132
+ * Defaults to true: `sessions.hosted.*` is handler-less without it, and a
133
+ * client with no terminal of its own has no other way to start a run.
134
+ */
135
+ readonly hostSessions?: boolean;
136
+ /**
137
+ * A mailbox address to watch. Omitted ⇒ none, and the inbound-mail
138
+ * composition honestly registers no `email.expectation.*` /
139
+ * `email.inbound.status` handler, exactly as it does on a daemon nobody has
140
+ * configured a mailbox for. Pass one to exercise the provisioned shape.
141
+ * Nothing connects: the composition reads the account name, and no source
142
+ * starts until the supervisor does.
143
+ */
144
+ readonly watchedMailbox?: string;
145
+ }
146
+
147
+ export interface DaemonFixture {
148
+ /** The composed runtime graph — the same object `DaemonServer` was handed. */
149
+ readonly services: RuntimeServices;
150
+ /** The running server. */
151
+ readonly daemon: DaemonServer;
152
+ /** `http://127.0.0.1:<bound port>`, valid after `start()` returned. */
153
+ readonly baseUrl: string;
154
+ /** The bearer token this daemon accepts. */
155
+ readonly token: string;
156
+ readonly homeDirectory: string;
157
+ readonly workingDirectory: string;
158
+ /** Fetch a path on this daemon with the bearer token attached. */
159
+ fetch(path: string, init?: RequestInit): Promise<Response>;
160
+ /**
161
+ * Fetch a path with NO credential. A route that exists answers 401; a path
162
+ * nothing serves answers 404 — which is what makes this the side-effect-free
163
+ * way to ask whether a route exists, even for a write verb.
164
+ */
165
+ fetchAnonymous(path: string, init?: RequestInit): Promise<Response>;
166
+ /** Invoke a gateway verb in-process, the way the control plane dispatches it. */
167
+ invoke<T = unknown>(methodId: string, body?: Record<string, unknown>): Promise<T>;
168
+ /** Stop the server, dispose the graph, and remove a temp root this created. */
169
+ stop(): Promise<void>;
170
+ }
171
+
172
+ /**
173
+ * Compose and start a daemon. Always `await fixture.stop()` — the runtime graph
174
+ * starts pollers while it builds, and abandoning it leaves every one of them
175
+ * firing for the rest of the process.
176
+ */
177
+ export async function startDaemonFixture(options: DaemonFixtureOptions = {}): Promise<DaemonFixture> {
178
+ const ownsRoot = options.root === undefined;
179
+ const root = options.root ?? mkdtempSync(join(tmpdir(), 'goodvibes-daemon-fixture-'));
180
+ const workingDirectory = join(root, 'workspace');
181
+ const homeDirectory = join(root, 'home');
182
+ const configDir = join(homeDirectory, '.goodvibes', 'daemon');
183
+ mkdirSync(workingDirectory, { recursive: true });
184
+ mkdirSync(configDir, { recursive: true });
185
+
186
+ const token = options.token ?? `daemon-fixture-${Math.random().toString(36).slice(2)}`;
187
+
188
+ const configManager = new ConfigManager({
189
+ surfaceRoot: 'tui',
190
+ configDir,
191
+ workingDir: workingDirectory,
192
+ homeDir: homeDirectory,
193
+ });
194
+ if (options.watchedMailbox !== undefined) {
195
+ configManager.set('surfaces.email.inbound.accounts', JSON.stringify([options.watchedMailbox]));
196
+ }
197
+ options.configure?.(configManager);
198
+
199
+ const featureFlags = createFeatureFlagManager();
200
+ const flags = deriveFeatureStates(configManager);
201
+ for (const id of options.featureFlagIds ?? DAEMON_CAPABILITY_FLAGS) flags[id] = 'enabled';
202
+ featureFlags.loadFromConfig({ flags });
203
+
204
+ const services = createRuntimeServices({
205
+ runtimeStore: createRuntimeStore(),
206
+ runtimeBus: new RuntimeEventBus(),
207
+ configManager,
208
+ workingDir: workingDirectory,
209
+ homeDirectory,
210
+ featureFlags,
211
+ getConversationTitle: () => 'daemon fixture',
212
+ });
213
+
214
+ // Ephemeral port: two concurrent test processes must never collide, and
215
+ // injecting a serveFactory also makes DaemonServer skip its pre-bind OS port
216
+ // probe (the facade only probes when serveFactory === Bun.serve).
217
+ let boundPort = 0;
218
+ const capturingServe = ((serveOptions) => {
219
+ const server = Bun.serve(serveOptions);
220
+ if (server.port !== undefined) boundPort = server.port;
221
+ return server;
222
+ }) as typeof Bun.serve;
223
+
224
+ const daemon = new DaemonServer({
225
+ port: 0,
226
+ host: '127.0.0.1',
227
+ userAuth: new UserAuthManager({
228
+ bootstrapFilePath: join(homeDirectory, 'auth-users.json'),
229
+ bootstrapCredentialPath: join(homeDirectory, 'auth-bootstrap.txt'),
230
+ users: [{ username: 'admin', passwordHash: UserAuthManager.hashPassword('admin'), roles: ['admin'] }],
231
+ }),
232
+ runtimeServices: services,
233
+ serveFactory: capturingServe,
234
+ // What the real entrypoint states, for the same reason it states it: the
235
+ // hosted-session verbs are registered by this composition and by nothing
236
+ // else, so a fixture that leaves it out is a daemon a client cannot start a
237
+ // session on — and every contract test written against it would agree.
238
+ ...(options.hostSessions === false ? {} : { hostedSessions: createHostedSessionOptions(services) }),
239
+ });
240
+
241
+ daemon.enable({ daemon: true }, token);
242
+ await daemon.start();
243
+ if (!daemon.isRunning) {
244
+ services.dispose();
245
+ if (ownsRoot) rmSync(root, { recursive: true, force: true });
246
+ throw new Error('daemon fixture: the server refused to start');
247
+ }
248
+
249
+ const baseUrl = `http://127.0.0.1:${boundPort}`;
250
+
251
+ return {
252
+ services,
253
+ daemon,
254
+ baseUrl,
255
+ token,
256
+ homeDirectory,
257
+ workingDirectory,
258
+ fetch(path, init) {
259
+ const headers = new Headers(init?.headers);
260
+ headers.set('Authorization', `Bearer ${token}`);
261
+ if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
262
+ return fetch(`${baseUrl}${path}`, { ...init, headers });
263
+ },
264
+ fetchAnonymous(path, init) {
265
+ return fetch(`${baseUrl}${path}`, init);
266
+ },
267
+ invoke<T>(methodId: string, body: Record<string, unknown> = {}): Promise<T> {
268
+ return services.gatewayMethods.invoke(methodId, { methodId, body } as never) as Promise<T>;
269
+ },
270
+ async stop(): Promise<void> {
271
+ await daemon.stop();
272
+ services.dispose();
273
+ if (ownsRoot) rmSync(root, { recursive: true, force: true });
274
+ },
275
+ };
276
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * hosted-session-failures.ts — what `sessions.hosted.*` answers when it refuses.
3
+ *
4
+ * ── Why this is a shipped module ─────────────────────────────────────────
5
+ *
6
+ * The platform's shared mock-daemon fixture set
7
+ * (@pellux/goodvibes-contracts/testing) is generated from each method's OUTPUT
8
+ * schema, so every fixture in it is a 200 carrying a schema-valid success body.
9
+ * There is no failure shape in it for anything, and a client mocked entirely
10
+ * from it has never once been handed a refusal.
11
+ *
12
+ * That matters most for hosted sessions, because a hosted session refuses in
13
+ * four distinct ways ON PURPOSE and the whole point of the distinction is that
14
+ * a client reacts differently to each: "no such session", "that session exists
15
+ * and cannot serve this right now, here is why", "you are at the configured
16
+ * cap", "your argument is malformed". A client that collapses them retries the
17
+ * one thing that will never work.
18
+ *
19
+ * These are the three refusals a client has to handle and could not previously
20
+ * mock. They are declared here rather than hand-written in each consumer, and
21
+ * `src/test/daemon/gateway-hosted-session-failures.test.ts` drives the REAL
22
+ * engine to produce each one and asserts it still matches — so a fixture that
23
+ * drifts from the daemon fails in this repository, not in a consumer's CI six
24
+ * weeks later.
25
+ */
26
+
27
+ /** The wire shape of a refusal, as `invokeGatewayMethodCall` reports it. */
28
+ export interface HostedSessionFailureFixture {
29
+ /** The verb whose refusal this is. */
30
+ readonly methodId: string;
31
+ /** Machine-readable code — the field a client branches on. */
32
+ readonly code: string;
33
+ /** HTTP status the control plane maps this refusal to. */
34
+ readonly status: number;
35
+ /** What the caller did to earn it, in one line. */
36
+ readonly when: string;
37
+ /** How a client should react. Stated because the codes exist to be reacted to. */
38
+ readonly clientAction: string;
39
+ }
40
+
41
+ /**
42
+ * A hosted session id that names nothing. 404: the id is wrong, and no amount
43
+ * of retrying it will make it right.
44
+ */
45
+ export const SESSION_NOT_FOUND: HostedSessionFailureFixture = {
46
+ methodId: 'sessions.hosted.attach',
47
+ code: 'SESSION_NOT_FOUND',
48
+ status: 404,
49
+ when: 'the sessionId names no session this daemon holds',
50
+ clientAction: 'Drop the id. Re-list before addressing a session again.',
51
+ };
52
+
53
+ /**
54
+ * The session is real and cannot serve this request. 409, deliberately not 404:
55
+ * a 404 would invite a retry against an id that is perfectly valid. The message
56
+ * carries the reason (terminated, loop not composable) so a client can say why.
57
+ */
58
+ export const HOSTED_SESSION_UNAVAILABLE: HostedSessionFailureFixture = {
59
+ methodId: 'sessions.hosted.attach',
60
+ code: 'HOSTED_SESSION_UNAVAILABLE',
61
+ status: 409,
62
+ when: 'the session exists but has terminated, or its loop could not be composed',
63
+ clientAction: 'Show the reason. Offer a new session, never a retry of this one.',
64
+ };
65
+
66
+ /**
67
+ * At the configured cap. 429: the request is well-formed and the daemon is not
68
+ * broken — there is no room. Retrying after a kill succeeds.
69
+ */
70
+ export const HOSTED_SESSION_LIMIT_REACHED: HostedSessionFailureFixture = {
71
+ methodId: 'sessions.hosted.create',
72
+ code: 'HOSTED_SESSION_LIMIT_REACHED',
73
+ status: 429,
74
+ when: 'hostedSessions.maxSessions live sessions already exist',
75
+ clientAction: 'Offer to kill one, or to raise hostedSessions.maxSessions. Retry after either.',
76
+ };
77
+
78
+ /** Every hosted-session refusal a client must handle, in one list. */
79
+ export const HOSTED_SESSION_FAILURE_FIXTURES: readonly HostedSessionFailureFixture[] = [
80
+ SESSION_NOT_FOUND,
81
+ HOSTED_SESSION_UNAVAILABLE,
82
+ HOSTED_SESSION_LIMIT_REACHED,
83
+ ];
84
+
85
+ /** Every `sessions.hosted.*` verb, for a conformance sweep's `onlyIds`. */
86
+ export const HOSTED_SESSION_METHOD_IDS: readonly string[] = [
87
+ 'sessions.hosted.create',
88
+ 'sessions.hosted.attach',
89
+ 'sessions.hosted.detach',
90
+ 'sessions.hosted.kill',
91
+ 'sessions.hosted.list',
92
+ ];
package/src/version.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ // Read version from package.json at runtime (eliminates build-time sync issues).
5
+ // Fallback for compiled binaries where package.json may not be present.
6
+ // The prebuild script updates the fallback value before compilation.
7
+ // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
+ // which is correct regardless of the process working directory.
9
+ let _version = '1.28.0';
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
+ // Only trust a version read from OUR OWN package.json. A Bun single-file
13
+ // compiled binary resolves import.meta.dir to a virtual root where the
14
+ // `../package.json` path can land on a DIFFERENT package.json (a bundled
15
+ // dependency's, or the embedded runtime's) that reports a placeholder like
16
+ // "0.0.0" — exactly the wrong-version banner a bare daemon launch showed in
17
+ // the field. Guarding on the package name means the prebuild-baked fallback
18
+ // above wins in that case instead of a stray version.
19
+ if (pkg?.name === 'goodvibes-daemon' && typeof pkg.version === 'string' && pkg.version.length > 0) {
20
+ _version = pkg.version;
21
+ }
22
+ } catch {
23
+ // Compiled binary or missing package.json — use fallback
24
+ }
25
+
26
+ export const VERSION = _version;