@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
@@ -1,8 +1,8 @@
1
1
  /**
2
- * cluster-composition.ts this daemon's seat in the LAN leader election.
2
+ * cluster-composition.ts, this daemon's seat in the LAN leader election.
3
3
  *
4
- * When the same goodvibes install runs more than once on one network a
5
- * laptop and a desktop, or two processes on one machine every copy
4
+ * When the same goodvibes install runs more than once on one network, a
5
+ * laptop and a desktop, or two processes on one machine, every copy
6
6
  * independently reads the shared inbox, so one message is picked up twice and
7
7
  * answered twice. The SDK's cluster coordinator elects exactly one node to be
8
8
  * responsible for inbound consumption; everything else stays warm and silent.
@@ -10,8 +10,8 @@
10
10
  * This file exists because this daemon does NOT get that for free from the
11
11
  * SDK facade. The facade gates the consumers IT owns (Telegram
12
12
  * ingress, the ntfy/Slack/Discord provider runtime), but this repository
13
- * composes an inbound consumer of its own the Slack/Discord/email inbox
14
- * poller in daemon/handlers/inbox and the facade knows nothing about it.
13
+ * composes an inbound consumer of its own, the Slack/Discord/email inbox
14
+ * poller in daemon/handlers/inbox, and the facade knows nothing about it.
15
15
  * A fix that only landed in the facade would leave the poller double-reading
16
16
  * on exactly the machine this product runs on.
17
17
  *
@@ -51,7 +51,7 @@ export interface GatedPollerControl {
51
51
  * Constructing it is inert: no socket is opened and no state is written until
52
52
  * `start()` runs, so composing a runtime in a test never joins a network.
53
53
  *
54
- * The version is THIS BINARY's version, not the SDK package's the same
54
+ * The version is THIS BINARY's version, not the SDK package's, the same
55
55
  * distinction daemon/cli.ts already makes for the self-update artifact. It is
56
56
  * the first ranking tier, so getting it wrong would let a stale build hold the
57
57
  * role through an update.
@@ -62,7 +62,7 @@ export function createClusterComposition(options: {
62
62
  * `readClusterSettings` asks for. The daemon passes a whole ConfigManager;
63
63
  * the handler context carries only this slice, and a test standing in a
64
64
  * minimal config object should not have to fabricate the full fifty-odd
65
- * members to compose a coordinator a cast there would hide a real shape
65
+ * members to compose a coordinator, a cast there would hide a real shape
66
66
  * mismatch rather than document one.
67
67
  */
68
68
  readonly configManager: Pick<ConfigManager, 'getCategory'>;
@@ -75,7 +75,7 @@ export function createClusterComposition(options: {
75
75
  * the current group key. That is what stops a daemon belonging to somebody
76
76
  * else on the same network from taking part in this election at all.
77
77
  *
78
- * Absent the plain default means the coordinator opens its own socket and
78
+ * Absent, the plain default, means the coordinator opens its own socket and
79
79
  * coordinates with anything that answers on the configured port.
80
80
  */
81
81
  readonly transport?: ClusterTransport | undefined;
@@ -96,7 +96,7 @@ export function createClusterComposition(options: {
96
96
  *
97
97
  * One gate per account, not one for the poller. Each inbox account is its own
98
98
  * surface in the election, so this laptop can hold the work Slack account
99
- * while the desktop holds the mailbox and losing one machine moves only the
99
+ * while the desktop holds the mailbox, and losing one machine moves only the
100
100
  * accounts it was reading. A single gate covering the whole poller could not
101
101
  * express that: it would hand every account over together, and a node with a
102
102
  * credential for only one of them could never take part at all.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * cluster-group-composition.ts this machine's membership of a LAN group.
2
+ * cluster-group-composition.ts, this machine's membership of a LAN group.
3
3
  *
4
4
  * Leader election answers "which of us reads the inbox". This answers the
5
5
  * question underneath it: "which machines are US". Without a group, any daemon
6
- * that happened to be on the same network a neighbour's, a colleague's, a
7
- * container someone left running would join the same coordination and one of
6
+ * that happened to be on the same network, a neighbour's, a colleague's, a
7
+ * container someone left running, would join the same coordination and one of
8
8
  * you would silently stop receiving messages.
9
9
  *
10
10
  * The group layer owns the socket and the election rides on it: every
@@ -77,7 +77,7 @@ export function createClusterGroupComposition(options: {
77
77
  * The leader election's own answer to "am I the master".
78
78
  *
79
79
  * Config replication needs exactly one machine issuing revisions, and it must
80
- * be the SAME machine leadership already picked two notions of master in
80
+ * be the SAME machine leadership already picked, two notions of master in
81
81
  * one process would disagree the moment one of them changed.
82
82
  *
83
83
  * Late-bound because the coordinator is built from this composition's
@@ -91,7 +91,7 @@ export function createClusterGroupComposition(options: {
91
91
  * in `cluster status` and in /status. Late-bound for the same reason
92
92
  * `isMaster` is. Absent means the group layer reports that the information is
93
93
  * unavailable rather than reporting an empty list as though this machine held
94
- * nothing a distinction an operator diagnosing a silent inbox depends on.
94
+ * nothing, a distinction an operator diagnosing a silent inbox depends on.
95
95
  */
96
96
  readonly surfaceHoldings?: (() => readonly ClusterSurfaceHolding[]) | undefined;
97
97
  }): ClusterGroupComposition {
@@ -181,7 +181,7 @@ export function createClusterGroupComposition(options: {
181
181
  * through a closure because the coordinator does not exist yet when the group
182
182
  * layer is constructed.
183
183
  *
184
- * Constructing either is inert no socket, no key material read until
184
+ * Constructing either is inert, no socket, no key material read, until
185
185
  * `startCluster` runs.
186
186
  */
187
187
  export function createClusterServices(options: {
@@ -228,7 +228,7 @@ export async function startClusterServices(services: {
228
228
  * Ask the group to take this machine back, on start.
229
229
  *
230
230
  * This is the zero-touch return. A machine that has been switched off for
231
- * months through many group-key rotations and possibly a join-key change
231
+ * months, through many group-key rotations and possibly a join-key change,
232
232
  * comes up holding stale keys, says "it is still me" with a key that never
233
233
  * rotates, and is re-keyed to the current generation by whichever member
234
234
  * answers. The operator does nothing.
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // conversation-rewind-port.ts the daemon's RewindConversationPort for the
2
+ // conversation-rewind-port.ts, the daemon's RewindConversationPort for the
3
3
  // SDK's unified rewind service.
4
4
  //
5
5
  // The SDK's UnifiedRewindService (platform/rewind) joins files rewind (workspace
@@ -9,7 +9,7 @@
9
9
  // truncation and captures the pre-/post-truncation snapshots so the reversal can
10
10
  // be undone and re-applied. The truncation boundary is the message count recorded
11
11
  // for the anchor's turnId at TURN_COMPLETED (the SDK's platform/rewind turn
12
- // anchors) the same
12
+ // anchors), the same
13
13
  // join key files rewind uses against the workspace checkpoint.
14
14
  //
15
15
  // This module is the daemon's implementation of that port, for sessions THIS
@@ -17,7 +17,7 @@
17
17
  // anchor.sessionId from the registry below.
18
18
  //
19
19
  // While conversation loops run in the surfaces, that registry is empty here, and
20
- // it used to answer an empty resolution with "0 messages to drop" the same
20
+ // it used to answer an empty resolution with "0 messages to drop", the same
21
21
  // answer a conversation already at the anchor gives, which is a confident wrong
22
22
  // answer rather than a missing one. It now reports the anchor as UNAVAILABLE
23
23
  // with the reason, which the SDK's rewind service turns into a plan warning.
@@ -25,7 +25,7 @@
25
25
  // The surfaces reach conversation rewind a different way now: they offer their
26
26
  // live conversation over the control plane (rewind.conversation.*), and the
27
27
  // SDK's host broker asks them directly. This port is the fallback the broker
28
- // falls through to for sessions no surface has offered which is exactly the
28
+ // falls through to for sessions no surface has offered, which is exactly the
29
29
  // case where the daemon hosts the conversation itself. Files-scope rewind,
30
30
  // entirely daemon-owned, was never affected either way.
31
31
  //
@@ -65,7 +65,7 @@ export interface ConversationRewindPort extends RewindConversationPort {
65
65
  restoreAfter(undoSnapshotId: string): boolean;
66
66
  }
67
67
 
68
- /** One truncation's captured state the target conversation and its snapshots. */
68
+ /** One truncation's captured state, the target conversation and its snapshots. */
69
69
  interface SnapshotPair {
70
70
  readonly conv: RewindableConversation;
71
71
  readonly before: ConversationJson;
@@ -80,7 +80,7 @@ const NO_LIVE_CONVERSATION =
80
80
  * Build a conversation rewind port. `resolveConversation` maps an anchor's
81
81
  * sessionId to the live conversation: the daemon looks the session up in the
82
82
  * registry below. A null resolution is reported as unavailable with the reason
83
- * never as a count, because "nobody here is holding those messages" and
83
+ *, never as a count, because "nobody here is holding those messages" and
84
84
  * "there are no messages to drop" are different facts and only one of them is
85
85
  * true.
86
86
  */
@@ -139,7 +139,7 @@ export function createConversationRewindPort(
139
139
  }
140
140
 
141
141
  // ---------------------------------------------------------------------------
142
- // Live per-session conversation registry the daemon-hosted mutable store the
142
+ // Live per-session conversation registry, the daemon-hosted mutable store the
143
143
  // composed daemon's rewind.plan/apply verbs fall back to. A process INSIDE this
144
144
  // daemon that runs a conversation registers it here; a surface in another
145
145
  // process offers its conversation over the control plane instead
@@ -162,7 +162,7 @@ export function unregisterSessionConversation(sessionId: string): void {
162
162
 
163
163
  /**
164
164
  * The conversation rewind port the composed daemon threads into
165
- * registerGatewayVerbGroups it resolves each anchor's live conversation from
165
+ * registerGatewayVerbGroups, it resolves each anchor's live conversation from
166
166
  * the registry above, so the daemon's own rewind verbs serve conversation scope
167
167
  * live in this process.
168
168
  */
@@ -7,7 +7,7 @@
7
7
  * These are grouped because they share one fact, and it is the fact this round
8
8
  * had to fix: WHERE the daemon's own credentials live. `daemonHome` is threaded
9
9
  * rather than defaulted, because without it a daemon told to run out of a temp
10
- * tree moved its identity directory and nothing else the credential store
10
+ * tree moved its identity directory and nothing else, the credential store
11
11
  * stayed in the real home, so an "isolated" test daemon held live credentials
12
12
  * and long-polled a real account. A default here would silently restore that.
13
13
  *
@@ -25,7 +25,7 @@ interface CredentialCompositionInput {
25
25
  readonly homeDirectory: string;
26
26
  /**
27
27
  * The daemon's state root when the host was told one. Absent means "not
28
- * overridden" and must stay absent rather than being defaulted see above.
28
+ * overridden" and must stay absent rather than being defaulted, see above.
29
29
  */
30
30
  readonly daemonHomeDirectory?: string | undefined;
31
31
  readonly configManager: ConstructorParameters<typeof SecretsManager>[0]['configManager'];
@@ -1,9 +1,9 @@
1
1
  /**
2
- * daemon-handler-composition.ts the daemon's HOST-side handler surfaces.
2
+ * daemon-handler-composition.ts, the daemon's HOST-side handler surfaces.
3
3
  *
4
4
  * Attaches this repository's handlers to the SDK-auto-registered builtin
5
5
  * gateway descriptors (channels.* / email.* / calendar.*) via
6
- * catalog.register(descriptor, handler, { replace: true }) the SDK owns
6
+ * catalog.register(descriptor, handler, { replace: true }), the SDK owns
7
7
  * every id, descriptor and schema; only the behaviour is ours. The remote
8
8
  * surface reuses the SAME DistributedRuntimeManager the SDK facade injects.
9
9
  *
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * The one behavioural decision that lives here: the inbox poller is handed to
14
14
  * the cluster coordinator instead of being started eagerly. It is this
15
- * product's own inbound consumer the SDK facade does not know it exists
15
+ * product's own inbound consumer, the SDK facade does not know it exists,
16
16
  * so if it is not gated here it is not gated anywhere, and two goodvibes nodes
17
17
  * on one network each read the shared inbox and answer the same message twice.
18
18
  */
@@ -20,6 +20,8 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
20
20
  import type { ClusterCoordinator } from '@pellux/goodvibes-sdk/platform/cluster';
21
21
  import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
22
22
  import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
23
+ import type { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
24
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
23
25
  import { registerDaemonHandlers, type DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
24
26
  import type { HandlerContext, HandlerLogger } from '../daemon/handlers/context.ts';
25
27
  import { createDaemonCredentialStore } from '../daemon/handlers/credentials.ts';
@@ -28,7 +30,10 @@ import { registerInboxMethods } from '../daemon/handlers/inbox/index.ts';
28
30
  import { registerTriagedInbox } from '../daemon/handlers/triage/index.ts';
29
31
  import { registerDraftMethods } from '../daemon/handlers/drafts/index.ts';
30
32
  import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
33
+ import { createPaymentsServices } from './payments-composition.ts';
31
34
  import { inboxPollerGate } from './cluster-composition.ts';
35
+ import type { ShellPathService } from '@/runtime/index.ts';
36
+ import type { BrowserCheckoutSeamHolder } from './browser-checkout-seam-holder.ts';
32
37
 
33
38
  export interface DaemonHandlerCompositionOptions {
34
39
  readonly gatewayMethods: GatewayMethodCatalog;
@@ -36,12 +41,23 @@ export interface DaemonHandlerCompositionOptions {
36
41
  readonly configManager: ConfigManager;
37
42
  readonly workingDirectory: string;
38
43
  readonly homeDirectory: string;
44
+ /** Resolves the surface-scoped control-plane paths the payment stores live at. */
45
+ readonly shellPaths: ShellPathService;
39
46
  readonly distributedRuntime: NonNullable<Parameters<typeof registerRemoteSurface>[1]>['manager'];
40
47
  /**
41
48
  * Decides whether THIS node polls the shared inbox. Always supplied by the
42
49
  * composition root; the poller is never started outside it.
43
50
  */
44
51
  readonly clusterCoordinator: ClusterCoordinator;
52
+ /**
53
+ * Where `payments.checkout.*` reads the browser-checkout seam, filled later
54
+ * by services.ts's own `onBrowserCheckout`. See
55
+ * runtime/browser-checkout-seam-holder.ts and payments-composition.ts's
56
+ * header for why this has to be a getter rather than the seam itself.
57
+ */
58
+ readonly checkoutSeam: BrowserCheckoutSeamHolder['get'];
59
+ readonly channelDeliveryRouter: Pick<ChannelDeliveryRouter, 'deliver'>;
60
+ readonly providerRegistry: Pick<ProviderRegistry, 'getCurrentModel' | 'getForModel'>;
45
61
  }
46
62
 
47
63
  export function createDaemonHandlerComposition(
@@ -65,12 +81,53 @@ export function createDaemonHandlerComposition(
65
81
  registerInbox: (ctx, routing) =>
66
82
  registerTriagedInbox(ctx, (inboxCtx) => registerInboxMethods(inboxCtx, routing, {
67
83
  // Hands polling to leadership. The `channels.inbox.list` read stays
68
- // available on every node a standby still SERVES the persisted feed,
84
+ // available on every node, a standby still SERVES the persisted feed,
69
85
  // it just does not FETCH into it.
70
86
  gatePolling: (providerId, control) =>
71
87
  options.clusterCoordinator.register(inboxPollerGate(providerId, control)),
72
88
  })).unregister,
73
89
  registerDrafts: (ctx) => registerDraftMethods(ctx),
90
+ // The payment stores need a path resolver and a daemon-scoped secret writer,
91
+ // neither of which is on HandlerContext, so they are built here and the
92
+ // provider only carries the teardown. See payments-composition.ts for the
93
+ // full checkout composition (address store, notifier, merchant judge,
94
+ // browser-checkout seam).
95
+ registerPayments: () => createPaymentsServices({
96
+ gatewayMethods: options.gatewayMethods,
97
+ configManager: options.configManager,
98
+ secretsManager: options.secretsManager,
99
+ shellPaths: options.shellPaths,
100
+ // True only on a machine that is not clustered at all, and false on every
101
+ // node of a cluster.
102
+ //
103
+ // `isMaster` was the obvious answer and it is the wrong one: it means
104
+ // "this node holds at least one inbound surface", which two nodes sharing
105
+ // a mailbox and a Slack workspace both satisfy, so a two-node cluster
106
+ // would have answered true twice. The SDK's gates.ts is explicit that
107
+ // exactly one node may act and that the wrong answer here is a
108
+ // double-spend.
109
+ //
110
+ // The alternative was to register a payments surface with the coordinator
111
+ // and read holdsSurface(), which is this repo's per-surface idiom
112
+ // (inboxPollerGate). It is the right answer once there is something to
113
+ // elect over, and today there is not: ClusterConsumerGate.start() is
114
+ // specified to not resolve until consumption has actually begun, and this
115
+ // composition has no payments consumer to start it. Checkout is now
116
+ // wired (payments.checkout.begin/.fillCard), but registering a gate whose
117
+ // start() does nothing would STILL put a fake consumer in the election and
118
+ // in `cluster status`; a real election is a separate piece of work, left
119
+ // for a later pass, not something wiring the checkout pair itself needed.
120
+ //
121
+ // So the honest reading of the topology stays what it was: clustering off
122
+ // means this is the only node, and it is trivially the one that would
123
+ // spend; clustering on means no payments election has been held and this
124
+ // node cannot claim to have won it. False on every node is also the safe
125
+ // direction, checkPaymentGates refuses on false.
126
+ isPaymentsLeader: () => !options.clusterCoordinator.enabled,
127
+ checkoutSeam: options.checkoutSeam,
128
+ channelDeliveryRouter: options.channelDeliveryRouter,
129
+ providerRegistry: options.providerRegistry,
130
+ }).unregister,
74
131
  registerRemote: (ctx) => registerRemoteSurface(ctx, { manager: options.distributedRuntime }),
75
132
  });
76
133
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * device-posture-composition.ts the paired-phone feature inside THIS daemon.
2
+ * device-posture-composition.ts, the paired-phone feature inside THIS daemon.
3
3
  *
4
4
  * A phone pairs with whichever daemon the person runs. Until this module
5
5
  * existed this daemon had no device posture at all: `device.nodes.maxPaired`
6
6
  * was enforced at the pairing path (SDK-side) and `device.capabilities.mode`
7
7
  * was an onboarding toggle, while the other eleven `device.*` keys were
8
- * recorded, read back, and governed nothing the capability service they
8
+ * recorded, read back, and governed nothing, the capability service they
9
9
  * describe was never built here, so there was nothing for them to govern.
10
10
  *
11
11
  * The feature itself is platform-owned (`platform/devices`): the settings→policy
@@ -13,16 +13,16 @@
13
13
  * confirmation flow, and the `phone` tool. This module supplies the three seams
14
14
  * that are actually ours and nothing else:
15
15
  *
16
- * - the peer transport the same DistributedRuntimeManager the remote surface
16
+ * - the peer transport, the same DistributedRuntimeManager the remote surface
17
17
  * pairs devices onto, so a phone paired here is a node here,
18
- * - the approval path the shared approval broker, so the confirmation
18
+ * - the approval path, the shared approval broker, so the confirmation
19
19
  * appears wherever the person is looking (terminal, web app, companion),
20
20
  * - the storage root and the live config manager.
21
21
  *
22
22
  * Constructing this starts nothing. `startHousekeeping()` is called from the
23
23
  * bootstrap tail: grants and captures both outlive a restart, so a grant whose
24
24
  * phone is gone, or a capture torn by a crash, is reaped BEFORE the first
25
- * request of this run is served and the periodic sweep after it is what keeps
25
+ * request of this run is served, and the periodic sweep after it is what keeps
26
26
  * a long-running daemon from going days without one. A failed sweep is logged
27
27
  * and the app still runs: housekeeping failing is a reason to say so, not a
28
28
  * reason to refuse to start.
@@ -52,8 +52,8 @@ export interface DevicePostureCompositionOptions {
52
52
  /**
53
53
  * Binding the catalog turns the whole devices.* family from
54
54
  * cataloged-but-unhandled into real handlers: nodes.list, grants.*,
55
- * housekeeping.run, and since a surface with no device runtime of its own
56
- * could read the grants and never open a camera capability.request and
55
+ * housekeeping.run, and, since a surface with no device runtime of its own
56
+ * could read the grants and never open a camera, capability.request and
57
57
  * artifacts.list/read. That is what makes the paired phone usable from the web
58
58
  * app and the companion rather than only through this process's own tool.
59
59
  *
@@ -85,8 +85,8 @@ export function createDevicePostureServices(options: DevicePostureCompositionOpt
85
85
 
86
86
  /**
87
87
  * Everything a host with a tool registry has to do once it is up: register the
88
- * `phone` tool the only path that reaches the capability service, so without it
89
- * the posture keys govern nothing a session can observe and start housekeeping.
88
+ * `phone` tool, the only path that reaches the capability service, so without it
89
+ * the posture keys govern nothing a session can observe, and start housekeeping.
90
90
  *
91
91
  * One call, because these two belong to the same feature and a host that did the
92
92
  * first and forgot the second would serve requests while never reaping a grant
@@ -103,7 +103,7 @@ export function installDevicePosture(
103
103
  /**
104
104
  * The recovery sweep plus the periodic timer. Separate from construction so
105
105
  * composing a runtime in a test starts no timer and touches no disk, and
106
- * separate from the call above so this daemon which registers no tools
106
+ * separate from the call above so this daemon, which registers no tools,
107
107
  * still sweeps.
108
108
  */
109
109
  export function startDeviceHousekeeping(devicePosture: DevicePostureRuntime): void {
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Teardown for every poller the daemon's runtime graph starts.
5
5
  *
6
- * The mechanics the ordered, best-effort, idempotent scope and the
7
- * all-required owner list live in the SDK (`platform/runtime/disposal`), so
6
+ * The mechanics, the ordered, best-effort, idempotent scope and the
7
+ * all-required owner list, live in the SDK (`platform/runtime/disposal`), so
8
8
  * the daemon and the SDK's own composition root cannot drift into two different
9
9
  * ideas of what "stop the graph" means. What lives here is only the mapping from
10
10
  * the daemon's assembled graph onto that list, plus the four pollers the daemon
@@ -19,7 +19,7 @@
19
19
  * Ownership note, and the reason any of this exists: this product builds the
20
20
  * graph and hands the SAME object to `DaemonServer`. The SDK facade disposes
21
21
  * only a graph it constructed itself, so it deliberately leaves this one alone
22
- * nothing upstream will ever stop these pollers. The shutdown paths in
22
+ *, nothing upstream will ever stop these pollers. The shutdown paths in
23
23
  * daemon/cli.ts and the one-shot CLI commands are the only things that can.
24
24
  *
25
25
  * The owner type below is declared structurally rather than imported from
@@ -44,7 +44,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
44
44
  /**
45
45
  * Daemon-only: the wake-word recovery sweep and a pending boot provision
46
46
  * Started only when an entrypoint opted into boot
47
- * provisioning, and a no-op otherwise but it is on this list unconditionally,
47
+ * provisioning, and a no-op otherwise, but it is on this list unconditionally,
48
48
  * because "the graph did not start it this time" is not a reason for the
49
49
  * teardown path to have no way to stop it.
50
50
  */
@@ -53,7 +53,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
53
53
  * Daemon-only: the product handler surfaces (daemon-handler-composition.ts).
54
54
  *
55
55
  * `unregister()` detaches the gateway handlers AND stops the two pollers this
56
- * product's inbox surface owns the retention sweep inside `InboxCursorStore`
56
+ * product's inbox surface owns, the retention sweep inside `InboxCursorStore`
57
57
  * and the per-provider `InboundPoller` intervals. The SDK does not know either
58
58
  * exists, so if this surface does not stop them nothing does.
59
59
  */
@@ -68,7 +68,7 @@ export interface DaemonRuntimePollerOwners extends Omit<RuntimePollerOwners, 'st
68
68
 
69
69
  /**
70
70
  * The poller owners this daemon holds that are NOT reachable from the
71
- * assembled graph handles the factory keeps as locals.
71
+ * assembled graph, handles the factory keeps as locals.
72
72
  */
73
73
  export interface RuntimeDisposalExtras {
74
74
  /** Handle returned by `ConfigManager.watchConfigFiles()`. */
@@ -79,7 +79,7 @@ export interface RuntimeDisposalExtras {
79
79
  * Register the stop call for every poller the graph started.
80
80
  *
81
81
  * `services` is the fully-assembled graph, which already exposes each poller
82
- * owner as a field so a poller whose owner reaches the public surface is
82
+ * owner as a field, so a poller whose owner reaches the public surface is
83
83
  * wired by name rather than by threading another local out of the factory.
84
84
  */
85
85
  export function registerDaemonRuntimePollers(
@@ -93,7 +93,7 @@ export function registerDaemonRuntimePollers(
93
93
  registry.add('wake-word housekeeping', services.stopWakeHousekeeping);
94
94
  // Registered LAST so it tears down FIRST (the scope unwinds in reverse), which
95
95
  // is the order daemon/cli.ts already used by hand: release the handler surfaces
96
- // closing the inbox store and stopping its poll timers before the pollers
96
+ //, closing the inbox store and stopping its poll timers, before the pollers
97
97
  // the rest of the graph owns. Being on this list is what makes a plain
98
98
  // `dispose()` total: every shutdown path stops these, not just the one that
99
99
  // remembered to call `unregister()` itself.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * fleet-needs-input-push.ts wires the fleet "needs-input" browser-push fan-out
2
+ * fleet-needs-input-push.ts, wires the fleet "needs-input" browser-push fan-out
3
3
  * that the SDK's registerGatewayVerbGroups (reached through the terminal-shell's
4
4
  * attachWsOnlyGatewayVerbHandlers wrapper) gates on two deps: `runtimeBus` and
5
5
  * `sessionPresence` (see push/service.ts's attachFleetNeedsInputSource). When a
6
6
  * fleet node blocks on the operator, it pushes an "Input needed" notification
7
- * carrying the session/node deep link to every registered device suppressed
7
+ * carrying the session/node deep link to every registered device, suppressed
8
8
  * while an operator surface is already attached to that node's session.
9
9
  *
10
10
  * TWO seams have to be live for a real push, not just descriptor presence:
@@ -12,8 +12,8 @@
12
12
  * runtime bus's 'fleet' domain in the first place. That is
13
13
  * attachFleetEmitBridge, diffing the process registry's own coalesced
14
14
  * snapshot tick into per-node lifecycle events. The terminal-shell package's
15
- * createArchivableFleetRegistry only builds the registry it does not
16
- * attach the bridge so the composition root that owns both the registry
15
+ * createArchivableFleetRegistry only builds the registry, it does not
16
+ * attach the bridge, so the composition root that owns both the registry
17
17
  * and the bus (this file) is the one place responsible for it. The SDK's
18
18
  * own daemon composition (platform/runtime/services.ts) attaches this same
19
19
  * bridge right after building its process registry.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * fleet-services.ts the shared, archive-aware fleet registry construction,
2
+ * fleet-services.ts, the shared, archive-aware fleet registry construction,
3
3
  * lifted out of services.ts so the composition root stays under the file-size
4
4
  * cap. Mirrors the sibling create*Services helpers (durability, code-index,
5
5
  * workstream): a single dependency-injected call that services.ts makes once.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * hosted-session-composition.ts what this daemon states so it may host
2
+ * hosted-session-composition.ts, what this daemon states so it may host
3
3
  * conversation loops.
4
4
  *
5
5
  * The engine is the SDK's (`@pellux/goodvibes-sdk/platform/hosted-sessions`):
6
6
  * lifecycle, the detach policy, the bounded disk state, the verbs, the
7
7
  * lifecycle channel. It is off until a product says how a workspace FLOOR is
8
- * built, and that is deliberate the floor's `requestApproval` seam is where a
8
+ * built, and that is deliberate, the floor's `requestApproval` seam is where a
9
9
  * product's trust posture lives, and no default can stand in for a decision
10
10
  * about who may write files and run commands in a directory a client named over
11
11
  * the wire.
@@ -20,8 +20,8 @@
20
20
  * session hosted in another directory has to be gated by that directory's
21
21
  * decision, not by the daemon's.
22
22
  *
23
- * Everything else in a floor comes from `createClientRuntimeServices` the
24
- * same composition a terminal runs so a hosted turn's tools, hooks, plugins
23
+ * Everything else in a floor comes from `createClientRuntimeServices`, the
24
+ * same composition a terminal runs, so a hosted turn's tools, hooks, plugins
25
25
  * and model stack are the ones the platform already has, not a second set.
26
26
  *
27
27
  * ── The exec posture a hosted turn runs under ──────────────────────────────
@@ -29,15 +29,15 @@
29
29
  * "The same composition a terminal runs" is what BUILDS the exec tool, and it
30
30
  * builds it the same way here: the `sandbox.*` config and the `exec-sandbox`
31
31
  * gate this daemon already reads produce the same bubblewrap boundary a local
32
- * exec gets network, PID, UTS and IPC namespaced, the system read-only, /tmp
32
+ * exec gets, network, PID, UTS and IPC namespaced, the system read-only, /tmp
33
33
  * and $HOME masked, the workspace writable, and `sandbox.egressAllowlist` the
34
- * one way network comes back with the self-labelling note on every result.
34
+ * one way network comes back, with the self-labelling note on every result.
35
35
  *
36
36
  * What was NOT the same was the fallback. When no boundary could be applied,
37
37
  * the exec tool ran the command on the host and said so, which is right for a
38
38
  * terminal (a person asked, a person is reading) and wrong for a turn nobody is
39
39
  * watching. A hosted conversational turn reached the whole host that way: the
40
- * full process table, the owner's /proc, and his tmux session where it typed.
40
+ * full process table, the owner's /proc, and his tmux session, where it typed.
41
41
  *
42
42
  * So this daemon states the posture rather than inheriting a default:
43
43
  *
@@ -46,8 +46,8 @@
46
46
  * REQUIRED: a command that cannot be contained is refused, naming why, and
47
47
  * running on the host is not a fallback available to it. The owner's
48
48
  * terminal is denied outright, at the exec guard, regardless of boundary.
49
- * - WORKSTREAM a real work chain the owner authorized, which may genuinely
50
- * need the machine itself is a per-spawn grant this composition makes
49
+ * - WORKSTREAM, a real work chain the owner authorized, which may genuinely
50
+ * need the machine itself, is a per-spawn grant this composition makes
51
51
  * explicitly. There is no such spawn today, so the function below never
52
52
  * returns it; when there is one, the grant is written HERE, in the daemon's
53
53
  * own composition, where a reader can see which spawns hold it. Nothing on
@@ -75,13 +75,13 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
75
75
  [
76
76
  'You are a GoodVibes session hosted by the daemon rather than by a terminal.',
77
77
  `Your working directory is ${input.workspaceRoot}.`,
78
- 'Someone may be attached and watching this turn, or may have detached and read it later',
78
+ 'Someone may be attached and watching this turn, or may have detached and read it later;',
79
79
  'write for both. Say what you did and why; never report work you did not do.',
80
80
  'Tool permissions are decided by whoever is attached: an ask you raise may take a while to be',
81
81
  'answered, and an unanswered one is a refusal, not a reason to find another way round.',
82
82
  ].join(' '),
83
83
  // The same contract every other conversational turn is held to, from the
84
- // SDK rather than restated here a second copy is a copy that drifts.
84
+ // SDK rather than restated here, a second copy is a copy that drifts.
85
85
  CONVERSATIONAL_DIAGNOSIS_SECTION,
86
86
  ].join('\n\n');
87
87
  }
@@ -92,7 +92,7 @@ function hostedSystemPrompt(input: { readonly workspaceRoot: string }): string {
92
92
  * Every session this daemon hosts is a conversation, so every one of them is
93
93
  * contained (see the module header). Stated on the floor rather than left to
94
94
  * the engine's default so that the day a workstream spawn exists, the grant is
95
- * a visible change to THIS function and not an option someone set elsewhere
95
+ * a visible change to THIS function and not an option someone set elsewhere,
96
96
  * and it sits beside the trust gate, which is the other statement this daemon
97
97
  * makes about how much authority a hosted run carries.
98
98
  */
@@ -162,7 +162,7 @@ export function createHostedSessionOptions(services: RuntimeServices): DaemonHos
162
162
  // boot (the persisted discovery cache) and from the LAN scan; a floor
163
163
  // builds its own registry and would otherwise be the only place on this
164
164
  // box where they are not routable. Servers found by a scan that finishes
165
- // AFTER a floor is built reach the next floor, not this one stated
165
+ // AFTER a floor is built reach the next floor, not this one, stated
166
166
  // rather than hidden, because a wrong claim here would look like a model
167
167
  // that exists everywhere except in hosted sessions.
168
168
  const discovered = services.providerRegistry.listDiscoveredServers();
@@ -15,7 +15,7 @@
15
15
  // Type-only: the runtime namespace objects must never be READ at module scope.
16
16
  // An eager `export const X = ns.X` compiles to a top-level property read off a
17
17
  // lazy namespace object, and Bun's single-file compiler orders module bodies
18
- // nondeterministically on some builds the read lands before the defining
18
+ // nondeterministically, on some builds the read lands before the defining
19
19
  // module and the binary dies at load. Values below are grouped live re-exports
20
20
  // from the SDK's registered subpaths instead; the toolchain post-build-smoke
21
21
  // scans compiled artifacts for the eager pattern and fails the build if one
@@ -1,11 +1,11 @@
1
1
  // ---------------------------------------------------------------------------
2
- // knowledge-services.ts the knowledge/wiki + home-graph stack
2
+ // knowledge-services.ts, the knowledge/wiki + home-graph stack
3
3
  //
4
4
  // Constructs the three KnowledgeStores (regular wiki, agent, home-graph), their
5
5
  // semantic services, the ingest services, the home-graph service, and the
6
6
  // project-planning + work-plan stores. Extracted into its own module rather
7
7
  // than built inline in services.ts, which sits at the architecture check's
8
- // 800-line cap (scripts/check-architecture.ts) new/large construction blocks
8
+ // 800-line cap (scripts/check-architecture.ts), new/large construction blocks
9
9
  // get their own module and a single wiring call there (mirrors
10
10
  // createWorkstreamServices / createDurabilityServices / createCodeIndexServices).
11
11
  //