@pellux/goodvibes-daemon 1.28.19 → 1.28.21

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 (114) hide show
  1. package/CHANGELOG.md +119 -63
  2. package/README.md +24 -19
  3. package/bin/launcher-support.js +5 -5
  4. package/package.json +6 -4
  5. package/scripts/postinstall.js +8 -8
  6. package/src/cli/command-catalog.ts +22 -22
  7. package/src/cli/completion.ts +4 -4
  8. package/src/cli/help.ts +5 -5
  9. package/src/cli/index.ts +3 -3
  10. package/src/cli/parser.ts +2 -2
  11. package/src/cli/surface-catalog.ts +1 -1
  12. package/src/cli/types.ts +2 -2
  13. package/src/cluster/daemon-ws-call.ts +5 -5
  14. package/src/cluster/raw-reply-route.ts +5 -5
  15. package/src/config/checkpoint-settings.ts +7 -7
  16. package/src/config/config-key-guard.ts +22 -0
  17. package/src/config/run-daemon-config-migration.ts +3 -3
  18. package/src/config/secret-config.ts +7 -7
  19. package/src/config/surface.ts +3 -3
  20. package/src/core/pairing-banner.ts +5 -5
  21. package/src/daemon/cli.ts +45 -43
  22. package/src/daemon/config-command.ts +15 -15
  23. package/src/daemon/handlers/context.ts +1 -1
  24. package/src/daemon/handlers/contracts.ts +19 -4
  25. package/src/daemon/handlers/credentials.ts +1 -1
  26. package/src/daemon/handlers/drafts/draft-store.ts +3 -3
  27. package/src/daemon/handlers/drafts/register.ts +4 -4
  28. package/src/daemon/handlers/inbox/aggregator.ts +8 -8
  29. package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
  30. package/src/daemon/handlers/inbox/index.ts +7 -7
  31. package/src/daemon/handlers/inbox/mapping.ts +2 -2
  32. package/src/daemon/handlers/inbox/poller.ts +5 -5
  33. package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
  34. package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
  35. package/src/daemon/handlers/inbox/providers/email.ts +3 -3
  36. package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
  37. package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
  38. package/src/daemon/handlers/index.ts +18 -8
  39. package/src/daemon/handlers/payments/address-store.ts +54 -0
  40. package/src/daemon/handlers/payments/budget-store.ts +356 -0
  41. package/src/daemon/handlers/payments/card-store.ts +486 -0
  42. package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
  43. package/src/daemon/handlers/payments/index.ts +38 -0
  44. package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
  45. package/src/daemon/handlers/payments/notifier.ts +112 -0
  46. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  47. package/src/daemon/handlers/payments/register.ts +518 -0
  48. package/src/daemon/handlers/register.ts +3 -3
  49. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  51. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  52. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  53. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  54. package/src/daemon/handlers/remote/index.ts +1 -1
  55. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  56. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  57. package/src/daemon/handlers/routing/index.ts +1 -1
  58. package/src/daemon/handlers/routing/route-store.ts +1 -1
  59. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  60. package/src/daemon/handlers/sqlite-store.ts +9 -9
  61. package/src/daemon/handlers/triage/index.ts +1 -1
  62. package/src/daemon/handlers/triage/integration.ts +3 -3
  63. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  64. package/src/daemon/handlers/triage/scorer.ts +2 -2
  65. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  66. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  67. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  68. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  69. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  70. package/src/daemon/handlers/triage/types.ts +2 -2
  71. package/src/daemon/lifecycle.ts +5 -5
  72. package/src/daemon/local-daemon-state.ts +7 -7
  73. package/src/daemon/pair-command.ts +14 -14
  74. package/src/daemon/provision-wake-model.ts +5 -5
  75. package/src/daemon/send/channels.ts +7 -7
  76. package/src/daemon/send/command.ts +11 -11
  77. package/src/daemon/send/composition.ts +5 -5
  78. package/src/daemon/send/failure-text.ts +6 -6
  79. package/src/daemon/send/inert-text.ts +18 -18
  80. package/src/daemon/send/stdin.ts +3 -3
  81. package/src/daemon/service-commands.ts +32 -32
  82. package/src/daemon/sessions-command.ts +7 -7
  83. package/src/daemon/status-command.ts +22 -22
  84. package/src/daemon/webui-command.ts +14 -14
  85. package/src/runtime/boot-tasks.ts +1 -1
  86. package/src/runtime/browser-checkout-seam-holder.ts +55 -0
  87. package/src/runtime/cluster-composition.ts +9 -9
  88. package/src/runtime/cluster-group-composition.ts +7 -7
  89. package/src/runtime/conversation-rewind-port.ts +8 -8
  90. package/src/runtime/credential-composition.ts +2 -2
  91. package/src/runtime/daemon-handler-composition.ts +61 -4
  92. package/src/runtime/device-posture-composition.ts +10 -10
  93. package/src/runtime/disposal-wiring.ts +8 -8
  94. package/src/runtime/fleet-needs-input-push.ts +4 -4
  95. package/src/runtime/fleet-services.ts +1 -1
  96. package/src/runtime/hosted-session-composition.ts +13 -13
  97. package/src/runtime/index.ts +1 -1
  98. package/src/runtime/knowledge-services.ts +2 -2
  99. package/src/runtime/legacy-daemon-migration.ts +43 -43
  100. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  101. package/src/runtime/mail-composition.ts +6 -6
  102. package/src/runtime/notification-dispatch.ts +7 -7
  103. package/src/runtime/payments-composition.ts +187 -0
  104. package/src/runtime/plugin-composition.ts +7 -7
  105. package/src/runtime/runtime-services-types.ts +9 -9
  106. package/src/runtime/services.ts +41 -32
  107. package/src/runtime/trigger-services.ts +1 -1
  108. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  109. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  110. package/src/runtime/update-check.ts +4 -4
  111. package/src/runtime/workspace-checkpointing.ts +6 -6
  112. package/src/testing/daemon-fixture.ts +11 -11
  113. package/src/testing/hosted-session-failures.ts +4 -4
  114. package/src/version.ts +2 -2
@@ -27,7 +27,7 @@ export interface SshBackendConfig {
27
27
  sshHost: string;
28
28
  sshPort?: number;
29
29
  sshUser: string;
30
- /** goodvibes://secrets/ reference to the private key never the raw key. */
30
+ /** goodvibes://secrets/ reference to the private key, never the raw key. */
31
31
  identityRef: string;
32
32
  }
33
33
 
@@ -71,7 +71,7 @@ export interface PeerRegistrationInput {
71
71
  }
72
72
 
73
73
  // ---------------------------------------------------------------------------
74
- // Validation helpers backendConfig must hold ONLY secret refs for any
74
+ // Validation helpers, backendConfig must hold ONLY secret refs for any
75
75
  // credential-bearing field. Raw secrets are rejected outright.
76
76
  // ---------------------------------------------------------------------------
77
77
 
@@ -111,9 +111,9 @@ function requireSecretRef(value: unknown, field: string): string {
111
111
  function assertDockerHostSafe(value: string | undefined, field: string): void {
112
112
  // A dockerHost is accepted in exactly two shapes, mirroring how docker.ts
113
113
  // resolves it (docker.ts: `startsWith('goodvibes://') ? resolveRef(...) : raw`):
114
- // 1. A goodvibes://secrets/ reference resolved from the credential store.
114
+ // 1. A goodvibes://secrets/ reference, resolved from the credential store.
115
115
  // 2. A credential-free local/plain address (unix:// socket, or a bare
116
- // tcp/host with no embedded userinfo) used verbatim.
116
+ // tcp/host with no embedded userinfo), used verbatim.
117
117
  // Enforcement here must match that resolution so no credential-bearing or
118
118
  // unresolvable value slips through to docker.ts.
119
119
  if (value === undefined) return;
@@ -123,7 +123,7 @@ function assertDockerHostSafe(value: string | undefined, field: string): void {
123
123
 
124
124
  // A `goodvibes://` value that is NOT a well-formed secret ref would be handed
125
125
  // to credentials.resolveRef() and fail opaquely (REMOTE_BACKEND_CREDENTIAL_MISSING)
126
- // or, worse, a near-miss could be treated as a literal host. Reject it at
126
+ //, or, worse, a near-miss could be treated as a literal host. Reject it at
127
127
  // registration so the misconfiguration surfaces immediately.
128
128
  if (isMalformedGoodVibesSecretReferenceValue(value)) {
129
129
  throw new PeerRegistryValidationError(
@@ -132,7 +132,7 @@ function assertDockerHostSafe(value: string | undefined, field: string): void {
132
132
  }
133
133
 
134
134
  // Embedded userinfo credentials (e.g. tcp://user:pass@host) must never be
135
- // stored raw docker.ts would pass them verbatim as DOCKER_HOST.
135
+ // stored raw, docker.ts would pass them verbatim as DOCKER_HOST.
136
136
  if (value.includes('@')) {
137
137
  throw new PeerRegistryValidationError(
138
138
  `Field '${field}' appears to embed credentials; pass a goodvibes://secrets/ reference instead.`,
@@ -226,7 +226,7 @@ export function normalizeBackendConfig(
226
226
  }
227
227
 
228
228
  // ---------------------------------------------------------------------------
229
- // Peer registry persisted via HandlerSqliteStore (peer-registry.sqlite)
229
+ // Peer registry, persisted via HandlerSqliteStore (peer-registry.sqlite)
230
230
  // ---------------------------------------------------------------------------
231
231
 
232
232
  const PEER_REGISTRY_FILE = 'peer-registry.sqlite';
@@ -247,21 +247,62 @@ interface PeerRow {
247
247
  backendConfig: string;
248
248
  }
249
249
 
250
- const VALID_BACKEND_KINDS: ReadonlySet<BackendKind> = new Set([
250
+ // Typed ReadonlySet<string> (not ReadonlySet<BackendKind>) so isBackendKind
251
+ // below can call .has() with a plain string and let the function's own `value
252
+ // is BackendKind` signature do the narrowing, instead of casting the set.
253
+ const VALID_BACKEND_KINDS: ReadonlySet<string> = new Set<BackendKind>([
251
254
  'docker',
252
255
  'ssh',
253
256
  'cloud-terminal',
254
257
  'local-process',
255
258
  ]);
256
259
 
260
+ /** True when `value` is one of the four known backend kinds; narrows to BackendKind. */
261
+ function isBackendKind(value: string): value is BackendKind {
262
+ return VALID_BACKEND_KINDS.has(value);
263
+ }
264
+
265
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
266
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
267
+ }
268
+
269
+ /**
270
+ * Turn a stored row back into a typed PeerRecord. backendConfig is written
271
+ * only by register() through normalizeBackendConfig(), but a row read back
272
+ * from disk is untrusted the same way a fresh registration's raw input is: a
273
+ * hand-edited database file, a row left over from a schema this file no
274
+ * longer writes, or on-disk corruption can all put something here that was
275
+ * never actually normalized. Re-running it through normalizeBackendConfig
276
+ * catches that at the read boundary instead of handing a malformed object to
277
+ * a caller that assumes register()'s guarantees already hold.
278
+ */
257
279
  function rowToRecord(row: PeerRow): PeerRecord {
258
- const backendKind = row.backendKind as BackendKind;
259
- const parsed = JSON.parse(row.backendConfig) as BackendConfig;
280
+ if (!isBackendKind(row.backendKind)) {
281
+ throw new PeerRegistryValidationError(
282
+ `Peer '${row.peerId}' has an unknown backendKind '${row.backendKind}'; the row is corrupt or from an unsupported version.`,
283
+ );
284
+ }
285
+ const backendKind = row.backendKind;
286
+
287
+ let raw: unknown;
288
+ try {
289
+ raw = JSON.parse(row.backendConfig);
290
+ } catch {
291
+ throw new PeerRegistryValidationError(
292
+ `Peer '${row.peerId}' has a backendConfig that is not valid JSON; the row is corrupt.`,
293
+ );
294
+ }
295
+ if (!isPlainObject(raw)) {
296
+ throw new PeerRegistryValidationError(
297
+ `Peer '${row.peerId}' has a backendConfig that is not an object; the row is corrupt.`,
298
+ );
299
+ }
300
+
260
301
  return {
261
302
  peerId: row.peerId,
262
303
  displayName: row.displayName,
263
304
  backendKind,
264
- backendConfig: parsed,
305
+ backendConfig: normalizeBackendConfig(backendKind, raw),
265
306
  };
266
307
  }
267
308
 
@@ -335,10 +376,18 @@ export class PeerRegistry {
335
376
  return rows.map(rowToRecord);
336
377
  }
337
378
 
338
- /** Remove a peer. Returns true when a row was deleted. */
379
+ /**
380
+ * Remove a peer. Returns true when a row was deleted. Existence is checked
381
+ * without row validation so a corrupt row (which get() rejects) can still
382
+ * be removed.
383
+ */
339
384
  async remove(peerId: string): Promise<boolean> {
340
385
  this.requireInit();
341
- const existed = this.get(peerId) !== null;
386
+ const row = this.store.all<{ peerId: string }>(
387
+ 'SELECT peerId FROM peers WHERE peerId = ?',
388
+ [peerId],
389
+ );
390
+ const existed = row.length > 0;
342
391
  if (existed) {
343
392
  this.store.run('DELETE FROM peers WHERE peerId = ?', [peerId]);
344
393
  await this.store.save();
@@ -9,22 +9,22 @@
9
9
  // `getProfileForChannel(surfaceKind, routeId?)` applies the canonical
10
10
  // resolution order:
11
11
  //
12
- // 1. exact surfaceKind AND routeId both match
13
- // 2. surface surfaceKind matches, route has no routeId
14
- // 3. wildcard a route with surfaceKind === 'any' (no routeId)
12
+ // 1. exact , surfaceKind AND routeId both match
13
+ // 2. surface , surfaceKind matches, route has no routeId
14
+ // 3. wildcard , a route with surfaceKind === 'any' (no routeId)
15
15
  //
16
16
  // This module bridges the two shapes. The bridge maps an inbound item's
17
17
  // `provider` to the routing `surfaceKind` and forwards an optional `routeId`
18
18
  // refinement; `fromDigest`/`kind` are NOT used as routing keys (the routing
19
19
  // store is keyed by surfaceKind + optional routeId only, never by sender digest
20
- // or message kind see RoutingChannelRoute). When no binding matches the
20
+ // or message kind, see RoutingChannelRoute). When no binding matches the
21
21
  // resolver returns null and the bridge yields `undefined`, so the adapter falls
22
22
  // back to leaving the item unrouted (offline/default behaviour intact). The
23
23
  // bridge NEVER throws: the underlying resolver is a pure in-memory lookup.
24
24
  //
25
25
  // The inbox surface is a separate follow-up module, so this bridge declares the
26
26
  // minimal `RouteResolver` seam locally rather than importing a sibling that the
27
- // routing slice does not own keeping the routing handler layer free of any
27
+ // routing slice does not own, keeping the routing handler layer free of any
28
28
  // cross-surface import edge.
29
29
  // ---------------------------------------------------------------------------
30
30
 
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Channel routing surface HANDLERS for the SDK-defined methods
2
+ // Channel routing surface, HANDLERS for the SDK-defined methods
3
3
  // - channels.routing.list (read-only)
4
4
  // - channels.routing.assign (admin, confirmation-gated)
5
5
  // - channels.routing.delete (admin, dangerous, confirmation-gated)
@@ -96,7 +96,7 @@ const SCHEMA: string[] = [
96
96
  updatedAt TEXT NOT NULL
97
97
  )`,
98
98
  `CREATE INDEX IF NOT EXISTS idx_routes_surface_route ON routes (surfaceKind, routeId)`,
99
- // Enforce one assignment per channel the upsert path relies on this.
99
+ // Enforce one assignment per channel, the upsert path relies on this.
100
100
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_routes_channel ON routes (channelId)`,
101
101
  ];
102
102
 
@@ -9,9 +9,9 @@ export const WILDCARD_SURFACE = 'any';
9
9
  * order as the agent-side `getProfileForChannel()` so offline and online
10
10
  * routing produce identical results:
11
11
  *
12
- * 1. exact match surfaceKind AND routeId both match
13
- * 2. surface-only surfaceKind matches, route has no routeId
14
- * 3. wildcard a route with surfaceKind === 'any' (no routeId)
12
+ * 1. exact match , surfaceKind AND routeId both match
13
+ * 2. surface-only , surfaceKind matches, route has no routeId
14
+ * 3. wildcard , a route with surfaceKind === 'any' (no routeId)
15
15
  *
16
16
  * Returns the matching profileId, or `null` when nothing matches.
17
17
  */
@@ -1,6 +1,6 @@
1
1
  /// <reference types="@pellux/goodvibes-sdk/sql-js" />
2
2
  // `sql.js` ships no types. The declaration is SDK-owned and reaches here
3
- // through that reference there is no local copy to keep in step.
3
+ // through that reference, there is no local copy to keep in step.
4
4
 
5
5
  import { mkdir, rename, writeFile } from 'node:fs/promises';
6
6
  import { readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
@@ -23,7 +23,7 @@ export interface SqliteStoreOptions {
23
23
 
24
24
  /**
25
25
  * The 16 bytes every SQLite file begins with ("SQLite format 3" + NUL). A file
26
- * that does not start with these is not a database, whatever its extension
26
+ * that does not start with these is not a database, whatever its extension,
27
27
  * which is exactly what a crash mid-create, a zero-fill, or a partially
28
28
  * restored backup leaves behind.
29
29
  */
@@ -93,7 +93,7 @@ export class HandlerSqliteStore {
93
93
  /**
94
94
  * Move a database file that cannot be opened aside instead of deleting it, so
95
95
  * an operator still has something to salvage, and return the quarantine path.
96
- * Returns null when the move itself failed in which case the caller starts
96
+ * Returns null when the move itself failed, in which case the caller starts
97
97
  * fresh in memory and the bad file is left exactly where it was rather than
98
98
  * being overwritten on the next save.
99
99
  */
@@ -110,8 +110,8 @@ export class HandlerSqliteStore {
110
110
  /**
111
111
  * Bound the quarantine directory: drop `.corrupt-*` copies of THIS store that
112
112
  * are past the TTL or beyond the keep-newest count. Best-effort and
113
- * idempotent a file another process already removed is success, not an
114
- * error so two daemons opening the same store at once cannot fight.
113
+ * idempotent, a file another process already removed is success, not an
114
+ * error, so two daemons opening the same store at once cannot fight.
115
115
  * Returns how many were reclaimed.
116
116
  */
117
117
  private reapCorruptQuarantines(nowMs: number): number {
@@ -158,7 +158,7 @@ export class HandlerSqliteStore {
158
158
 
159
159
  // Validate the file by its CONTENT, not by existsSync. `save()` writes
160
160
  // through a pid-and-timestamp temp file and an atomic rename, so it cannot
161
- // itself leave a half-written database but a zero-filled file recovered
161
+ // itself leave a half-written database, but a zero-filled file recovered
162
162
  // by a filesystem, a truncated restore, or a copy interrupted by something
163
163
  // outside this process all produce a path that exists and holds no usable
164
164
  // database. Handing those bytes to `new SQL.Database(...)` throws out of
@@ -200,7 +200,7 @@ export class HandlerSqliteStore {
200
200
  }
201
201
  } catch (error) {
202
202
  // Header present but the body does not hold up: a truncated or damaged
203
- // database. Same treatment set aside, disclose, start clean.
203
+ // database. Same treatment, set aside, disclose, start clean.
204
204
  quarantineReason = `database would not open: ${error instanceof Error ? error.message : String(error)}`;
205
205
  this.db = null;
206
206
  quarantined = this.quarantineUnreadable();
@@ -215,7 +215,7 @@ export class HandlerSqliteStore {
215
215
  // Disclosure: starting a store from scratch is data loss from the user's
216
216
  // point of view, so it is never allowed to happen quietly.
217
217
  if (quarantineReason) {
218
- logger.warn('daemon store could not be opened starting a fresh one', {
218
+ logger.warn('daemon store could not be opened; starting a fresh one', {
219
219
  store: this.options.fileName,
220
220
  path: this.resolvedPath,
221
221
  reason: quarantineReason,
@@ -266,7 +266,7 @@ export class HandlerSqliteStore {
266
266
  * saves of the same store in the SAME millisecond produced the same temp path
267
267
  * before it did: both wrote it, the first rename moved it away, and the
268
268
  * second failed with ENOENT on a file it had just written. That is not a
269
- * hypothetical race the inbox poller flushes once per provider and polls
269
+ * hypothetical race, the inbox poller flushes once per provider and polls
270
270
  * every provider concurrently, so it hit on an ordinary two-provider startup,
271
271
  * and the failure surfaced as a provider reporting a filesystem error for its
272
272
  * feed. Every store on this base shares the hazard, so the counter lives here.
@@ -5,7 +5,7 @@
5
5
  // runtime composition root calls. It DECORATES the inbox surface's
6
6
  // `channels.inbox.list` handler (overlaying persisted triage metadata) and
7
7
  // returns the poller-facing pipeline + tagger. inbox.triage.* are NOT published
8
- // catalog methods this surface registers no triage method id.
8
+ // catalog methods, this surface registers no triage method id.
9
9
  // ---------------------------------------------------------------------------
10
10
 
11
11
  export {
@@ -12,12 +12,12 @@
12
12
  // overlaid with the persisted triageScore/triageTags via
13
13
  // enrichItemsWithTriage(). Every other registration passes straight
14
14
  // through to the real catalog. The inbox descriptor/schema/id is never
15
- // re-authored only its handler is wrapped.
15
+ // re-authored, only its handler is wrapped.
16
16
  // 2. It exposes the triage pipeline + tagger (`runInboxTriage`, `tagger`) for
17
17
  // the daemon-internal poller, which scores items and persists them to the
18
18
  // co-located inbox-triage.sqlite store the decorator reads from.
19
19
  // 3. inbox.triage.* are intentionally NOT registered on the catalog (they are
20
- // a daemon-internal pipeline, not published methods) so this module
20
+ // a daemon-internal pipeline, not published methods), so this module
21
21
  // makes ZERO catalog.register call for any triage id.
22
22
  //
23
23
  // Reads in the decorator are best-effort and degrade to the raw item when no
@@ -51,7 +51,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
51
51
 
52
52
  /**
53
53
  * Canonical id of the inbox list method whose handler we decorate. This is the
54
- * SDK's published id referenced as a plain string for matching during
54
+ * SDK's published id, referenced as a plain string for matching during
55
55
  * registration; no descriptor or schema is authored here.
56
56
  */
57
57
  export const INBOX_LIST_METHOD_ID = 'channels.inbox.list';
@@ -57,7 +57,7 @@ export interface RunInboxTriageOptions {
57
57
  scorer?: TriageScorerOptions;
58
58
  /** Inject a store (tests). When omitted, a triage store is opened/closed. */
59
59
  store?: HandlerSqliteStore;
60
- /** When true, do not persist only compute (used by inbox.triage.list). */
60
+ /** When true, do not persist, only compute (used by inbox.triage.list). */
61
61
  dryRun?: boolean;
62
62
  /** Clock injection for deterministic updatedAt in tests. */
63
63
  now?: () => Date;
@@ -254,7 +254,7 @@ export function enrichItemsWithTriage<T extends { id: string }>(
254
254
  items: readonly T[],
255
255
  ): Array<T & TriageOverlay> {
256
256
  if (items.length === 0) return [];
257
- // Single batched read (`WHERE id IN (...)`) instead of one SELECT per item
257
+ // Single batched read (`WHERE id IN (...)`) instead of one SELECT per item,
258
258
  // this is a hot read path (every channels.inbox.list call), so the N+1 is
259
259
  // collapsed to one query keyed by id.
260
260
  const byId = readTriageMetadataBatch(
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Pure, dependency-free heuristic + naive-Bayes-style spam/priority scoring
5
5
  // over an InboundChannelItem's textual surface (subject + snippet). No I/O,
6
- // no credentials, no network safe to run on every polled item.
6
+ // no credentials, no network, safe to run on every polled item.
7
7
  //
8
8
  // The score is a normalized 0..1 confidence in the assigned label. The label
9
9
  // is one of 'spam' | 'priority' | 'normal'. Scoring is deterministic so the
@@ -247,7 +247,7 @@ export function scoreInboundItem(
247
247
  // A trailing question mark suggests an awaited answer.
248
248
  if (/\?\s*$/.test(raw)) priorityLL += 0.5;
249
249
 
250
- // Spam strongly suppresses priority promotional text is rarely urgent.
250
+ // Spam strongly suppresses priority, promotional text is rarely urgent.
251
251
  priorityLL -= spam * 2.5;
252
252
 
253
253
  const priority = clamp01(sigmoid(priorityLL));
@@ -1,10 +1,10 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Triage tagger Discord provider.
2
+ // Triage tagger, Discord provider.
3
3
  //
4
4
  // Two paths, in priority order:
5
5
  // 1. REAL thread tags: when the item targets a forum/media-channel thread and
6
6
  // a forum-tag mapping resolves >=1 tag id, PATCH the thread's applied_tags
7
- // (read-then-merge never blind overwrite). Exact fidelity to the
7
+ // (read-then-merge, never blind overwrite). Exact fidelity to the
8
8
  // contract's "Discord thread tags".
9
9
  // 2. Reaction analog: Discord has no arbitrary per-message tags, so otherwise
10
10
  // add a unicode reaction (PUT .../reactions/{emoji}/@me).
@@ -114,7 +114,7 @@ async function applyDiscordThreadTags(
114
114
  // DATA-LOSS GUARD: we PATCH the thread's full applied_tags array, so we must
115
115
  // first read the EXISTING tags and merge. If that read fails (network error
116
116
  // OR a non-ok HTTP status), we have no idea what tags are currently on the
117
- // thread PATCHing with only our new ids would silently destroy whatever
117
+ // thread, PATCHing with only our new ids would silently destroy whatever
118
118
  // forum tags were already applied. Abort instead of overwriting.
119
119
  const existing = await fetchDiscordAppliedTags(url, token, fetchImpl, ctx);
120
120
  if (existing === null) {
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Triage tagger IMAP provider.
2
+ // Triage tagger, IMAP provider.
3
3
  //
4
4
  // Applies a triage label as an IMAP keyword flag (STORE +FLAGS) over a minimal
5
5
  // IMAP4rev1-over-TLS client. Credentials are resolved per-apply from the daemon
@@ -63,7 +63,7 @@ export type ImapConnect = (opts: {
63
63
  servername: string;
64
64
  }) => ImapSocketLike;
65
65
 
66
- /** Production connector a real IMAP4rev1-over-TLS socket via node:tls. */
66
+ /** Production connector, a real IMAP4rev1-over-TLS socket via node:tls. */
67
67
  export const tlsImapConnect: ImapConnect = (opts) =>
68
68
  tlsConnect(opts, () => {
69
69
  /* greeting handled in the data pump */
@@ -128,7 +128,7 @@ export class ImapStoreError extends Error {
128
128
  * in a value destined for an IMAP command line. CR/LF are the dangerous ones:
129
129
  * an unescaped CRLF would terminate the current command and let an attacker
130
130
  * inject a second IMAP command (CRLF injection). IMAP's quoted-string syntax
131
- * has no escape for these control chars backslash only escapes `\` and `"` —
131
+ * has no escape for these control chars, backslash only escapes `\` and `"`,
132
132
  * so the only safe handling is to refuse the value outright.
133
133
  */
134
134
  function assertImapSafe(value: string, field: string): void {
@@ -155,8 +155,8 @@ function quoteImap(value: string): string {
155
155
  // A concrete UID sequence-set: one or more comma-separated single UIDs or
156
156
  // numeric ranges (`12`, `3:9`, `1,4,7:9`). RFC 3501 also permits `*` (the
157
157
  // largest UID in the mailbox) and ranges to `*`, but a wildcard would let a
158
- // single value like `1:*` apply the flag to the ENTIRE mailbox never the
159
- // intent when tagging one triaged item so `*` is rejected outright.
158
+ // single value like `1:*` apply the flag to the ENTIRE mailbox, never the
159
+ // intent when tagging one triaged item, so `*` is rejected outright.
160
160
  const IMAP_UID_SET = /^[0-9]+(:[0-9]+)?(,[0-9]+(:[0-9]+)?)*$/;
161
161
 
162
162
  // An IMAP flag: an optional leading `\` (system flag, e.g. `\Seen`) followed by
@@ -187,7 +187,7 @@ function assertImapUid(value: string): void {
187
187
  * UNQUOTED into the `+FLAGS (...)` list. assertImapSafe only blocks control
188
188
  * chars; a flag containing a space (e.g. `\Seen Junk`) would still pass that
189
189
  * guard and inject a second flag atom. This is the boundary check that
190
- * prevents it independent of any upstream normalizer.
190
+ * prevents it, independent of any upstream normalizer.
191
191
  */
192
192
  function assertImapFlag(value: string): void {
193
193
  if (!IMAP_FLAG.test(value)) {
@@ -313,7 +313,7 @@ export function imapStoreFlagOverTls(
313
313
  finish();
314
314
  }
315
315
  } else {
316
- // NO/BAD: deterministic protocol rejection do not retry.
316
+ // NO/BAD: deterministic protocol rejection, do not retry.
317
317
  current.reject(new ImapStoreError(`IMAP command failed: ${status}`, false));
318
318
  }
319
319
  };
@@ -159,7 +159,7 @@ export function createTriageTagger(
159
159
  return { ...base, reason: 'autotag-disabled' };
160
160
  }
161
161
 
162
- // Provider-side mutation is effectful require explicit confirmation.
162
+ // Provider-side mutation is effectful, require explicit confirmation.
163
163
  if (request.confirm !== true || request.explicitUserRequest !== true) {
164
164
  throw new HandlerError(
165
165
  'Provider-side triage tagging requires explicit user confirmation.',
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Triage tagger shared types and provider-agnostic helpers.
2
+ // Triage tagger, shared types and provider-agnostic helpers.
3
3
  //
4
4
  // Provider config shapes, the apply request/result contract, and the tag
5
5
  // normalization helpers used by the IMAP/Slack/Discord modules. No I/O here.
@@ -17,7 +17,7 @@ export interface TaggerProviderConfig {
17
17
  * optional forum-tag mapping. When `forumTagIds` maps a GoodVibes triage tag
18
18
  * (e.g. 'GoodVibes/Spam') to a forum tag SNOWFLAKE id, items that target a
19
19
  * forum/media-channel thread get that REAL thread tag applied (PATCH
20
- * applied_tags) exact fidelity to the contract's "Discord thread tags".
20
+ * applied_tags), exact fidelity to the contract's "Discord thread tags".
21
21
  * Without a mapping (or for non-thread messages) tagging degrades to a
22
22
  * unicode reaction analog.
23
23
  */
@@ -29,7 +29,7 @@ export interface ApplyTagsRequest {
29
29
  /** Provider-side tags to apply. Defaults to [labelToTag(label)] when omitted. */
30
30
  tags?: readonly string[];
31
31
  label?: TriageLabel;
32
- /** Must be true provider-side mutation requires explicit confirmation. */
32
+ /** Must be true, provider-side mutation requires explicit confirmation. */
33
33
  confirm?: boolean;
34
34
  /** Mirror of the operator invocation context flag. */
35
35
  explicitUserRequest?: boolean;
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Triage tagger Slack provider.
2
+ // Triage tagger, Slack provider.
3
3
  //
4
4
  // Applies a triage label as a Slack message reaction (reactions.add). The bot
5
5
  // token is resolved per-apply from the daemon credential store and is never
@@ -5,7 +5,7 @@
5
5
  // and the triage pipeline scores. It is NOT an SDK catalog contract: the
6
6
  // published `channels.inbox.list` output schema (CHANNEL_INBOX_ITEM_SCHEMA) is
7
7
  // owned by the SDK and never re-declared here. This is the internal poller
8
- // item per the handoff doc it carries `fromDigest` (never a raw sender id),
8
+ // item per the handoff doc, it carries `fromDigest` (never a raw sender id),
9
9
  // `subjectPreview`/`bodyPreview` (PII-stripped, length-bounded) and an opaque
10
10
  // `metadata` bag the tagger reads provider targeting from (imapUid, channelId,
11
11
  // Slack ts, Discord messageId/threadId).
@@ -31,7 +31,7 @@ export interface InboundChannelItem {
31
31
  /** Handoff-facing provider id ("slack" | "discord" | "email" | ...). */
32
32
  readonly provider?: string;
33
33
  readonly kind?: 'dm' | 'thread' | 'mention' | 'reaction';
34
- /** SHA-256 first-N of sender external id NEVER a raw identifier. */
34
+ /** SHA-256 first-N of sender external id, NEVER a raw identifier. */
35
35
  readonly fromDigest?: string;
36
36
  /** Conversation id (Slack/Discord channel, IMAP mailbox-scoped). */
37
37
  readonly conversationId?: string;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lifecycle.ts resolve THIS binary's update-artifact identity for the SDK
2
+ * lifecycle.ts, resolve THIS binary's update-artifact identity for the SDK
3
3
  * DaemonServer facade's auto-update lifecycle.
4
4
  *
5
5
  * The SDK facade now runs the entire self-update loop itself when handed a
@@ -7,12 +7,12 @@
7
7
  * version against the release tags (no longer the version-blind sdk-package
8
8
  * comparison that used to restart-loop), swaps only at an idle moment, keeps
9
9
  * the outgoing file at `<path>.previous`, and leaves a receipt in the store it
10
- * serves on /status. When the artifact is ABSENT, updates are host-managed
10
+ * serves on /status. When the artifact is ABSENT, updates are host-managed,
11
11
  * the facade runs no loop (the safe embedded default).
12
12
  *
13
13
  * The one guard the facade does NOT apply is install-kind: it must never swap a
14
14
  * dev `bun run daemon` interpreter or a bun-global package install. This helper
15
- * is that guard it hands the facade an artifact ONLY for a compiled binary
15
+ * is that guard, it hands the facade an artifact ONLY for a compiled binary
16
16
  * install, so a dev run resolves to `undefined` (host-managed, no loop) and
17
17
  * only a real self-contained binary self-updates.
18
18
  */
@@ -23,13 +23,13 @@ import { detectInstallKind } from '../runtime/update-check.ts';
23
23
  export interface ResolveDaemonUpdateArtifactOptions {
24
24
  /** The executable to identify; defaults to process.execPath. */
25
25
  readonly execPath?: string;
26
- /** Injectable so tests pin a fixture version never the live build VERSION. */
26
+ /** Injectable so tests pin a fixture version, never the live build VERSION. */
27
27
  readonly version?: string;
28
28
  }
29
29
 
30
30
  /**
31
31
  * The update-artifact identity to hand the DaemonServer facade, or `undefined`
32
- * for a non-binary install (dev/source or bun-global package) in which case
32
+ * for a non-binary install (dev/source or bun-global package), in which case
33
33
  * the facade keeps updates host-managed and runs no swap loop.
34
34
  */
35
35
  export function resolveDaemonUpdateArtifact(
@@ -1,5 +1,5 @@
1
1
  /**
2
- * local-daemon-state.ts the two files a daemon writes about ITSELF.
2
+ * local-daemon-state.ts, the two files a daemon writes about ITSELF.
3
3
  *
4
4
  * `status` and `update` want to report things no control-plane verb answers:
5
5
  * how long the daemon has been up, whether the last start followed a crash,
@@ -11,7 +11,7 @@
11
11
  * <control-plane config dir>/control-plane/daemon-receipts.json
12
12
  *
13
13
  * They are READ here and never written, and the receipts are never marked
14
- * delivered `/status?receipts=consume` hands each receipt to the first
14
+ * delivered, `/status?receipts=consume` hands each receipt to the first
15
15
  * consuming reader exactly once, and a status command that quietly consumed
16
16
  * them would take them away from the surface they were written for.
17
17
  *
@@ -22,7 +22,7 @@
22
22
  * The SDK owns the writers (`platform/daemon/lifecycle-marker.ts`,
23
23
  * `platform/daemon/receipts.ts`) and its readers are not exported from the
24
24
  * published package this repository pins, so the readers below are this
25
- * repository's own bounded and content-validated the same way, and no more
25
+ * repository's own, bounded and content-validated the same way, and no more
26
26
  * trusting of the file than the SDK is. The shared-piece lane can re-point them
27
27
  * at the SDK's own readers once those are exported.
28
28
  */
@@ -42,7 +42,7 @@ export interface DaemonLifecycleMarker {
42
42
  readonly pid: number | undefined;
43
43
  readonly failedStarts: number;
44
44
  readonly version: string | undefined;
45
- /** The version an automatic rollback moved AWAY from the build that crash looped. */
45
+ /** The version an automatic rollback moved AWAY from, the build that crash looped. */
46
46
  readonly rejectedVersion: string | undefined;
47
47
  /** When an automatic rollback last restored the kept previous binary. */
48
48
  readonly autoRollbackAt: number | undefined;
@@ -164,7 +164,7 @@ export function readDaemonReceipts(
164
164
  }
165
165
 
166
166
  export interface LocalDaemonState {
167
- /** False when the caller asked about another machine nothing below was read. */
167
+ /** False when the caller asked about another machine, nothing below was read. */
168
168
  readonly available: boolean;
169
169
  /** Why it is unavailable, when it is. */
170
170
  readonly unavailableReason: string | undefined;
@@ -195,7 +195,7 @@ export function describeLocalDaemonState(input: DescribeLocalDaemonStateInput):
195
195
  return {
196
196
  available: false,
197
197
  unavailableReason:
198
- 'the uptime, update receipts and rollback state are read from files on the daemon\'s own host '
198
+ 'the uptime, update receipts and rollback state are read from files on the daemon\'s own host; '
199
199
  + 'run this command on that machine to see them',
200
200
  marker: null,
201
201
  receipts: [],
@@ -219,7 +219,7 @@ export function describeLocalDaemonState(input: DescribeLocalDaemonStateInput):
219
219
  };
220
220
  }
221
221
 
222
- /** `3d 4h`, `4h 12m`, `12m 3s`, `9s` two units, never more. */
222
+ /** `3d 4h`, `4h 12m`, `12m 3s`, `9s`, two units, never more. */
223
223
  export function formatDuration(milliseconds: number): string {
224
224
  const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
225
225
  const days = Math.floor(totalSeconds / 86_400);