@pellux/goodvibes-daemon 1.28.7 → 1.28.10

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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,76 @@ All notable changes to the GoodVibes daemon.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.28.10] - 2026-08-05
10
+
11
+ ### Changes
12
+
13
+ - **Fixed: this daemon's answers match its own contract.** Profile field
14
+ reads leaked internal properties past a strict schema, so up-to-date
15
+ clients refused every profile answer and the platform could not tell its
16
+ owner his own address. Responses are explicit wire projections now, and a
17
+ conformance test runs the real routes against the contract (platform
18
+ runtime 2.0.8).
19
+ - **Fixed: one control-plane store.** A path defect made every boot rewrite
20
+ a legacy store nothing reads; state that still lived only at the legacy
21
+ location (occasions, workspace registrations) migrates to the daemon's
22
+ surface-scoped store on boot with a receipt, duplicates are quarantined
23
+ with receipts, and the legacy directory empties and is removed.
24
+ - **Changed: the daemon hosts conversation turns for the agent.** Hosted
25
+ session event streams are render-grade and session-scoped, so a client
26
+ rendering from the stream sees what the model said and what it did — and
27
+ agent conversations become genuinely cross-visible with their messages.
28
+ - Changed: connecting Google is one action, with every needed scope in a
29
+ single consent and a live mail-and-calendar proof at the end; the settings
30
+ catalog now carries the calendar/google/email keys this daemon really
31
+ reads (platform runtime 2.0.8).
32
+
33
+ ## [1.28.9] - 2026-08-02
34
+
35
+ ### Changes
36
+
37
+ - Changed: a conversational turn is told to understand what it captures, not
38
+ just file it — an itinerary also means an away-span said back in plain
39
+ words, travelers who are people in the owner's life, and durable facts
40
+ about the destination, and the turn uses what it stored: it names
41
+ collisions with existing plans and offers the obviously useful next steps
42
+ once (a reminder before departure, weather at the destination). Anything
43
+ beyond the conversation is still proposed and waits for a yes (platform
44
+ runtime 2.0.7).
45
+
46
+ ## [1.28.8] - 2026-08-02
47
+
48
+ ### Changes
49
+
50
+ - **Fixed: a chat message gets an answer, never workflow paperwork.** The
51
+ conversation gate's decision that a channel message is conversation is now
52
+ authoritative — the review-wording heuristic that turned "I'll review the
53
+ route" in a transcript into a full write-review-fix-confirm chain can no
54
+ longer override it. When a chain does legitimately run, the reply sent back
55
+ over the channel carries what the agent actually found or did; chain status
56
+ lines stay in the operator progress feed. Duplicate replies from the two
57
+ completion reporters are gone (platform runtime 2.0.6).
58
+ - **Added: personal information shared in conversation gets captured.** A
59
+ conversational channel turn — which previously ran with no tools at all —
60
+ now carries the profile capture tool with per-run owner authority: a trip
61
+ itinerary pasted into Telegram lands in the owner profile's Plans section
62
+ with its dates, flights, travelers and confirmation number, and the reply
63
+ states concretely what was stored. Configurable via
64
+ `profile.conversationalCapture` (default on) and `profile.ownerChannels`
65
+ (defaults to the occasions nudge channel).
66
+ - **Fixed: a chat session is filed as a chat.** Channel-originated sessions
67
+ are classified by their channel instead of as TUI project sessions rooted
68
+ in a filesystem path, rollover re-derives the classification, and a sender
69
+ on the channel's owner allowlist is attributed as the owner principal
70
+ instead of unknown. The daemon now hands its channel ingress policy to the
71
+ session intake so that attribution works end to end.
72
+ - Fixed: this daemon's settings file is rewritten only by this daemon.
73
+ Clients reading the daemon settings tier migrate their in-memory view and
74
+ leave the file bytes untouched, so a newer client can no longer rewrite the
75
+ file out from under an older running daemon; the daemon's own migration
76
+ folds the rename, the receipt, and a reader floor into one atomic write
77
+ (platform runtime 2.0.6).
78
+
9
79
  ## [1.28.7] - 2026-08-02
10
80
 
11
81
  ### Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-daemon/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-daemon/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-1.28.7-blue.svg)](https://github.com/mgd34msu/goodvibes-daemon)
5
+ [![Version](https://img.shields.io/badge/version-1.28.10-blue.svg)](https://github.com/mgd34msu/goodvibes-daemon)
6
6
 
7
7
  The GoodVibes daemon: one long-running process per machine that holds the control plane every
8
8
  GoodVibes client talks to. It answers the operator verb families over HTTP, reads and replies on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-daemon",
3
- "version": "1.28.7",
3
+ "version": "1.28.10",
4
4
  "description": "The GoodVibes daemon \u2014 the one long-running host for the control plane, channels, cluster membership, scheduled work, knowledge and memory stores, and the verb families every GoodVibes client calls.",
5
5
  "type": "module",
6
6
  "main": "src/daemon/cli.ts",
@@ -65,8 +65,8 @@
65
65
  "@anthropic-ai/bedrock-sdk": "^0.28.1",
66
66
  "@anthropic-ai/sdk": "^0.82.0",
67
67
  "@ast-grep/napi": "^0.42.0",
68
- "@pellux/goodvibes-sdk": "2.0.5",
69
- "@pellux/goodvibes-terminal-shell": "2.0.5",
68
+ "@pellux/goodvibes-sdk": "2.0.8",
69
+ "@pellux/goodvibes-terminal-shell": "2.0.8",
70
70
  "bash-language-server": "^5.6.0",
71
71
  "fuse.js": "^7.1.0",
72
72
  "graphql": "^16.13.2",
@@ -102,6 +102,13 @@ export interface RuntimeServicesOptions {
102
102
  export interface RuntimeServices {
103
103
  readonly workingDirectory: string;
104
104
  readonly homeDirectory: string;
105
+ /**
106
+ * The `.goodvibes/<surface root>/` segment this daemon's own state lives
107
+ * under — always GOODVIBES_DAEMON_SURFACE_ROOT here (config/surface.ts).
108
+ * Declared so a consumer ASKS for it instead of deriving a second one; the
109
+ * unscoped pre-split control-plane store is what deriving it twice produced.
110
+ */
111
+ readonly surfaceRoot: string;
105
112
  /** The declare-once session-storage handle every session reader and writer threads through. */
106
113
  readonly surface: SessionSurface;
107
114
  readonly shellPaths: ShellPathService;
@@ -2,9 +2,10 @@ import { join } from 'node:path';
2
2
  import { ServiceRegistry, SubscriptionManager, ToolLLM } from '@pellux/goodvibes-sdk/platform/config';
3
3
  import { AutomationDeliveryManager, AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
4
4
  import { ChannelPolicyManager } from '@pellux/goodvibes-sdk/platform/channels';
5
- import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker, buildSharedSessionAgentSpawnRoutingInput } from '@pellux/goodvibes-sdk/platform/control-plane';
5
+ import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker, buildSharedSessionAgentSpawnRoutingInput, controlPlaneStorePath } from '@pellux/goodvibes-sdk/platform/control-plane';
6
6
  import { AcpHostService } from '@pellux/goodvibes-sdk/platform/acp';
7
7
  import { continuationChainOptions } from '@pellux/goodvibes-sdk/platform/agents';
8
+ import { PersonalCaptureHolder, conversationalTurnSpawnOptions } from '@pellux/goodvibes-sdk/platform/personal-capture';
8
9
  import { resolvePairingWebOrigin } from '@pellux/goodvibes-sdk/platform/pairing';
9
10
  import { attachWsOnlyGatewayVerbHandlers } from '@pellux/goodvibes-terminal-shell';
10
11
  import { composeMailDeps } from './mail-composition.ts';
@@ -131,7 +132,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
131
132
  const { secretsManager, stepUpService, pairingTokens } = composeCredentialServices({
132
133
  workingDirectory, homeDirectory, configManager,
133
134
  daemonHomeDirectory: options.daemonHomeDirectory,
134
- pairingTokenPath: shellPaths.resolveUserPath('control-plane', 'pairing-tokens.json'),
135
+ pairingTokenPath: controlPlaneStorePath(shellPaths, GOODVIBES_DAEMON_SURFACE_ROOT, 'pairing-tokens.json'),
135
136
  });
136
137
  const subscriptionManager = new SubscriptionManager(shellPaths.resolveUserPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'subscriptions.json'));
137
138
  const serviceRegistry = new ServiceRegistry(shellPaths.resolveProjectPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'services.json'), {
@@ -205,6 +206,13 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
205
206
  } = createAgentGraph({
206
207
  runtimeBus: options.runtimeBus, workingDirectory, configManager, providerRegistry,
207
208
  });
209
+ // The one late-binding holder for the personal-capture port. The gateway verb
210
+ // groups fill it (they own the owner-profile store and the occasions service),
211
+ // and the agent orchestrator reads it when it builds a run's tool registry.
212
+ // Registration happens before setDependencies in this file, but the registry
213
+ // is built per run, so by the time a conversational turn asks for `profile`
214
+ // the port is already in place.
215
+ const personalCapture = new PersonalCaptureHolder();
208
216
  const hookDispatcher = new HookDispatcher({ agentManager, toolLLM, projectRoot: workingDirectory }, hookActivityTracker);
209
217
  configManager.attachHookDispatcher(hookDispatcher);
210
218
  const hookWorkbench = createHookWorkbench({
@@ -238,11 +246,23 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
238
246
  getCategory: (name: string) => configManager.getCategory(name as never),
239
247
  },
240
248
  }),
249
+ // What the answering agent is given and what it is told. `restrictTools:
250
+ // true` with no tool list used to mean an EMPTY registry: the agent could
251
+ // emit text and nothing else, which is why a trip pasted into a channel
252
+ // was answered and stored nowhere. This names the tools a conversational
253
+ // turn actually needs (read, find, fetch, and the `profile` capture tool),
254
+ // supplies the instruction that makes recording part of answering, and
255
+ // carries the write authority for the surface the message arrived on.
256
+ // It also supplies the run's context, which is why the bare
257
+ // `context: shared-session:<id>` line that used to sit below is gone.
258
+ // Spread BEFORE the routing builder so an explicit tool list coming from
259
+ // a routing intent still wins — that builder emits `tools` only when it
260
+ // actually has one.
261
+ ...conversationalTurnSpawnOptions(input, { configReader: configManager }),
241
262
  // Spawn routing through the SDK's shared model-reference resolver
242
263
  // (unique-across-registry auto-qualifies; ambiguous and unknown ids throw
243
264
  // errors naming real candidates), against the live registry's models.
244
265
  ...buildSharedSessionAgentSpawnRoutingInput(input.routing, { restrictTools: true, modelCandidates: providerRegistry.listModels() }),
245
- context: `shared-session:${input.sessionId}`,
246
266
  });
247
267
  return { agentId: record.id };
248
268
  });
@@ -506,11 +526,22 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
506
526
  // calendar.*/email.* are platform-served; these two let it register (mail-composition.ts).
507
527
  const { emailServiceDeps, describeEmailConfigProblem } = composeMailDeps({ configManager, secretsManager });
508
528
  attachWsOnlyGatewayVerbHandlers(gatewayMethods, {
529
+ // The surface segment every control-plane store path is built from
530
+ // (SDK control-plane-store-paths.ts). Required, not defaulted: a default
531
+ // is what let these stores write to the unscoped orphan directory.
532
+ surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
509
533
  homeDirectory, emailServiceDeps, describeEmailConfigProblem, processRegistry,
510
534
  // The registration-gated surface, not the raw manager: an explicit create in
511
535
  // an unregistered workspace refuses with something actionable.
512
536
  workspaceCheckpointManager: checkpointing.gatewayManager,
513
537
  conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService,
538
+ // Fills the capture port with the owner-profile store and the occasions
539
+ // service, so the `profile` tool a conversational turn is given has
540
+ // somewhere real to write.
541
+ personalCapture,
542
+ // The session intake attributes a sender on this policy's owner allowlist
543
+ // to the owner principal instead of unknown.
544
+ channelPolicy,
514
545
  approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input),
515
546
  // approvals.raise — a surface CREATING an ask in this broker. Without it the
516
547
  // verb is cataloged and unhandled, and a client whose prompt runs outside
@@ -596,6 +627,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
596
627
  sandboxSessionRegistry,
597
628
  workflowServices: workflow,
598
629
  contextAccountingHolder,
630
+ // Without this the `profile` tool is never registered, and a conversational
631
+ // turn that was told to record what the owner said has nothing to call.
632
+ personalCapture,
599
633
  });
600
634
 
601
635
  // Continuity reads (recovery-file presence, last-session pointer) scoped to
@@ -628,6 +662,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
628
662
  const services: RuntimeServices = {
629
663
  workingDirectory,
630
664
  homeDirectory,
665
+ surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
631
666
  surface,
632
667
  shellPaths,
633
668
  workspaceTrustManager,
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolveWorkspaceRegisterReadPath } from '@pellux/goodvibes-sdk/platform/workspace';
2
3
  import type { ShellPathService } from '@/runtime/index.ts';
3
4
  import {
4
5
  normalizeWorkspaceRoot,
@@ -30,9 +31,24 @@ import {
30
31
 
31
32
  export type StoreShellPaths = Pick<ShellPathService, 'resolveUserPath' | 'homeDirectory'>;
32
33
 
33
- /** Path of the shared store's JSON document — the same path the SDK's gateway verb group constructs its own store over. */
34
+ /**
35
+ * Path of the shared store's JSON document — the same path the SDK's gateway
36
+ * verb group constructs its own store over.
37
+ *
38
+ * "The same path" is the whole contract, and it is why this goes through the
39
+ * SDK's resolver rather than spelling any location out. goodvibes-agent reads
40
+ * and writes this same file directly and the SDK's gateway writes it, so the
41
+ * register is cross-product state rather than the daemon's own — it lives in
42
+ * the shared tier (~/.goodvibes/shared/), which takes no surface root, so all
43
+ * three resolve one identical path. Until the boot fold has moved it, the
44
+ * resolver falls back to the pre-split location read-only, so this reader never
45
+ * reports an empty register and silently refuses every checkpoint.
46
+ */
34
47
  export function sharedWorkspaceRegistrationStorePath(shellPaths: StoreShellPaths): string {
35
- return shellPaths.resolveUserPath('control-plane', 'workspace-registrations.json');
48
+ // READ path: the shared tier when it is there, the pre-split location until
49
+ // the daemon's boot fold has moved it. This is a reader, so the fallback is
50
+ // read-only and nothing here ever writes to the legacy address.
51
+ return resolveWorkspaceRegisterReadPath(shellPaths, existsSync);
36
52
  }
37
53
 
38
54
  function isRecord(value: unknown): value is Record<string, unknown> {
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.28.7';
9
+ let _version = '1.28.10';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  // Only trust a version read from OUR OWN package.json. A Bun single-file