@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -1,96 +1,36 @@
1
- export const GLYPHS = {
2
- frame: {
3
- topLeft: '┌',
4
- topRight: '┐',
5
- bottomLeft: '└',
6
- bottomRight: '┘',
7
- horizontal: '─',
8
- vertical: '│',
9
- teeLeft: '├',
10
- teeRight: '┤',
11
- teeTop: '┬',
12
- teeBottom: '┴',
13
- cross: '',
14
- },
15
- surface: {
16
- top: '▄',
17
- bottom: '▀',
18
- cursor: '█',
19
- altCursor: '▌',
20
- },
21
- navigation: {
22
- selected: '▸',
23
- collapsed: '▸',
24
- expanded: '▾',
25
- up: '↑',
26
- down: '↓',
27
- moreAbove: '▲',
28
- moreBelow: '▼',
29
- next: '→',
30
- back: '←',
31
- pipeSeparator: '│',
32
- },
33
- status: {
34
- success: '✓',
35
- failure: '✕',
36
- pending: '•',
37
- active: '●',
38
- idle: '○',
39
- info: '•',
40
- blocked: '⊘',
41
- skipped: '◇',
42
- review: '◈',
43
- retry: '↻',
44
- handoff: '⇢',
45
- reference: '↗',
46
- partial: '◐',
47
- dualPane: '◆',
48
- star: '★',
49
- },
50
- meter: {
51
- filled: '█',
52
- medium: '▓',
53
- light: '▒',
54
- empty: '░',
55
- spark: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
56
- },
57
- } as const;
1
+ // ---------------------------------------------------------------------------
2
+ // ui-primitives.ts — glyph registry + tone-token table.
3
+ //
4
+ // W4-R4: these four tables (GLYPHS, UI_TONES, DIFF_TONES, SPINNER_FRAMES) are
5
+ // no longer minted locally. They are the SDK presentation contract
6
+ // (@pellux/goodvibes-sdk/platform/presentation, landed by W4-S1), consumed here
7
+ // so the agent and the TUI share ONE source (Mike's move-to-SDK ruling —
8
+ // machinery needed by 2+ surfaces => SDK). See
9
+ // docs/decisions/2026-07-05-presentation-contract-sdk-extraction.md in the SDK.
10
+ //
11
+ // Re-exported under the historical names (GLYPHS, UI_TONES) so every existing
12
+ // importer keeps working with no call-site churn. UI_TONES is the dark-mode
13
+ // tone table (== resolveTones('dark')); light is resolved via theme.ts's
14
+ // activeUiTones() / resolveUiTones(), which composes the mode dimension over
15
+ // the SDK's resolveTones().
16
+ //
17
+ // Visible glyph convergence (deliberate, per S1's divergence ruling): the
18
+ // agent's status glyphs adopt the TUI reference — idle ○ (U+25CB) -> ◌ (U+25CC),
19
+ // info • (U+2022) -> ○ (U+25CB), and a new warn ⚠ key. Called out here so the
20
+ // render-time change is not mistaken for a regression.
21
+ // ---------------------------------------------------------------------------
58
22
 
59
- export const UI_TONES = {
60
- fg: {
61
- primary: '#e2e8f0',
62
- secondary: '#cbd5e1',
63
- muted: '#94a3b8',
64
- dim: '#475569',
65
- inverse: '#0f172a',
66
- },
67
- bg: {
68
- base: '#11131a',
69
- surface: '#161a22',
70
- title: '#0f172a',
71
- section: '#18202b',
72
- summary: '#1b2430',
73
- selected: '#223049',
74
- input: '#1e293b',
75
- warning: '#2b2116',
76
- error: '#2a161b',
77
- success: '#14241b',
78
- },
79
- state: {
80
- info: '#38bdf8',
81
- good: '#22c55e',
82
- warn: '#f59e0b',
83
- bad: '#ef4444',
84
- blocked: '#f97316',
85
- active: '#60a5fa',
86
- },
87
- accent: {
88
- browser: '#7dd3fc',
89
- control: '#22d3ee',
90
- inspector: '#c4b5fd',
91
- workflow: '#fbbf24',
92
- conversation: '#93c5fd',
93
- },
94
- } as const;
23
+ import {
24
+ GLYPHS,
25
+ TONE_TOKENS,
26
+ DIFF_TONES,
27
+ SPINNER_FRAMES,
28
+ } from '@pellux/goodvibes-sdk/platform/presentation';
95
29
 
30
+ export { GLYPHS, DIFF_TONES, SPINNER_FRAMES };
31
+
32
+ /** The dark-mode tone-token table (== resolveTones('dark')). */
33
+ export const UI_TONES = TONE_TOKENS;
34
+
35
+ /** The glyph registry shape, preserved for existing type references. */
96
36
  export type UiGlyphRegistry = typeof GLYPHS;
@@ -518,6 +518,21 @@ export async function initializeBootstrapCore(
518
518
  },
519
519
  }).catch((err) => { logger.debug('session broker create session failed at bootstrap', { err }); });
520
520
 
521
+ // W2A: mirror the create into the daemon spine (fire-and-forget; the local
522
+ // broker above stays the source of truth). kind:'agent' is the canonical record
523
+ // identity; participant.surfaceKind stays 'service' (the TRANSPORT axis).
524
+ services.sessionSpineClient.register({
525
+ sessionId: runtime.sessionId,
526
+ project: workingDir,
527
+ title: 'GoodVibes Agent session',
528
+ });
529
+ // Debounced heartbeat off turn activity — coalesced to one wire call per window,
530
+ // no title, reopen:false. Uses the ref so a resumed session id is followed.
531
+ runtimeUnsubs.push(
532
+ uiServices.events.turns.on('TURN_SUBMITTED', () => services.sessionSpineClient.heartbeat(runtimeSessionIdRef.value)),
533
+ uiServices.events.turns.on('TURN_COMPLETED', () => services.sessionSpineClient.heartbeat(runtimeSessionIdRef.value)),
534
+ );
535
+
521
536
  domainDispatch.syncSessionState({
522
537
  id: userSessionId,
523
538
  projectRoot: workingDir,
@@ -9,6 +9,7 @@ import { HelperModel } from '@pellux/goodvibes-sdk/platform/config';
9
9
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
10
10
  import { formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
11
11
  import type { SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
12
+ import type { SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
12
13
  import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
13
14
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
14
15
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
@@ -21,6 +22,8 @@ export interface ResumeSessionOptions {
21
22
  readonly requestRender: () => void;
22
23
  readonly onSessionIdChanged?: (sessionId: string) => void;
23
24
  readonly sharedSessionBroker: Pick<SharedSessionBroker, 'reopenSession'>;
25
+ readonly sessionSpineClient: Pick<SessionSpineClient, 'reopen'>;
26
+ readonly projectRoot: string;
24
27
  readonly writeLastSessionPointer: (sessionId: string) => void;
25
28
  readonly hookDispatcher: HookDispatcher;
26
29
  readonly sessionManager: SessionManager;
@@ -51,6 +54,9 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
51
54
  if (meta?.provider) options.runtime.provider = meta.provider;
52
55
  options.writeLastSessionPointer(sessionId);
53
56
  void options.sharedSessionBroker.reopenSession(sessionId).catch((err) => { logger.debug('session broker reopen session failed', { err }); });
57
+ // W2A: mirror the reopen into the daemon spine — reopen:true is sent ONLY on
58
+ // this explicit user resume verb (fire-and-forget; never blocks the resume).
59
+ options.sessionSpineClient.reopen({ sessionId, project: options.projectRoot });
54
60
  options.conversation.log(`Resumed session: ${sessionId}`, { fg: '135' });
55
61
  const returnContextMode = getReturnContextMode(options.configManager);
56
62
  if (returnContextMode !== 'off' && meta.returnContext) {
@@ -131,6 +131,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
131
131
  requestRender,
132
132
  onSessionIdChanged,
133
133
  sharedSessionBroker: services.sessionBroker,
134
+ sessionSpineClient: services.sessionSpineClient,
135
+ projectRoot: services.shellPaths.workingDirectory,
134
136
  writeLastSessionPointer,
135
137
  hookDispatcher: services.hookDispatcher,
136
138
  sessionManager: services.sessionManager,
@@ -27,8 +27,10 @@ import {
27
27
  loadLastConversation,
28
28
  writeLastSessionPointer,
29
29
  } from '@/runtime/index.ts';
30
- import { scheduleBackgroundMcpDiscovery, startBackgroundProviderRegistration } from '@/runtime/index.ts';
30
+ import { scheduleBackgroundMcpDiscovery } from '@/runtime/index.ts';
31
31
  import { restoreSavedModel } from '@/runtime/index.ts';
32
+ import { runGatedLanScan } from './lan-scan-consent.ts';
33
+ import { scheduleCalendarSubscriptionBootRefresh } from './calendar-boot-refresh.ts';
32
34
  import type { ExternalServicesHandle, HostServiceStatus, TurnEvent } from '@/runtime/index.ts';
33
35
  import type { UiRuntimeServices } from './ui-services.ts';
34
36
  import { createDeferredStartupCoordinator } from '@/runtime/index.ts';
@@ -37,6 +39,7 @@ import { createBootstrapShell } from './bootstrap-shell.ts';
37
39
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
38
40
  import { startMcpConfigAutoReload } from '../mcp/runtime-reload.ts';
39
41
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
42
+ import { foldLegacySpineStore } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
40
43
  import { AgentPromptContextReceiptStore, composeRuntimePromptWithReceipt } from '../agent/prompt-context-receipts.ts';
41
44
  import { registerAgentAuditTool } from '../tools/agent-audit-tool.ts';
42
45
  import { registerAgentAutonomyTool } from '../tools/agent-autonomy-tool.ts';
@@ -200,9 +203,17 @@ export async function bootstrapRuntime(
200
203
  services.shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'prompt-context-receipts.jsonl'),
201
204
  );
202
205
  let activePromptTurnId: string | null = null;
206
+ // The current turn's raw text (Wave-4 W4-A1B): the injection seam
207
+ // (composeRuntimePromptWithReceipt, invoked from getSystemPrompt below) needs the
208
+ // active turn's intent to rank the already-eligible memory set by relevance. This is
209
+ // the real seam W4-A1 left unthreaded — TURN_SUBMITTED already carries `prompt`, it
210
+ // was just never captured. Mirrors activePromptTurnId's lifecycle exactly: set on
211
+ // TURN_SUBMITTED, cleared on every terminal event for that turn.
212
+ let activePromptTurnText: string | null = null;
203
213
  runtimeUnsubs.push(
204
214
  runtimeBus.on<Extract<TurnEvent, { type: 'TURN_SUBMITTED' }>>('TURN_SUBMITTED', (event) => {
205
215
  activePromptTurnId = event.payload.turnId;
216
+ activePromptTurnText = event.payload.prompt;
206
217
  }),
207
218
  runtimeBus.on<Extract<TurnEvent, { type: 'TURN_COMPLETED' }>>('TURN_COMPLETED', (event) => {
208
219
  promptContextReceipts.recordTurnOutcome({
@@ -211,7 +222,10 @@ export async function bootstrapRuntime(
211
222
  terminalEvent: 'TURN_COMPLETED',
212
223
  stopReason: event.payload.stopReason,
213
224
  });
214
- if (activePromptTurnId === event.payload.turnId) activePromptTurnId = null;
225
+ if (activePromptTurnId === event.payload.turnId) {
226
+ activePromptTurnId = null;
227
+ activePromptTurnText = null;
228
+ }
215
229
  }),
216
230
  runtimeBus.on<Extract<TurnEvent, { type: 'TURN_ERROR' }>>('TURN_ERROR', (event) => {
217
231
  promptContextReceipts.recordTurnOutcome({
@@ -221,7 +235,10 @@ export async function bootstrapRuntime(
221
235
  stopReason: event.payload.stopReason,
222
236
  detail: event.payload.error,
223
237
  });
224
- if (activePromptTurnId === event.payload.turnId) activePromptTurnId = null;
238
+ if (activePromptTurnId === event.payload.turnId) {
239
+ activePromptTurnId = null;
240
+ activePromptTurnText = null;
241
+ }
225
242
  }),
226
243
  runtimeBus.on<Extract<TurnEvent, { type: 'TURN_CANCEL' }>>('TURN_CANCEL', (event) => {
227
244
  promptContextReceipts.recordTurnOutcome({
@@ -231,7 +248,10 @@ export async function bootstrapRuntime(
231
248
  stopReason: event.payload.stopReason,
232
249
  detail: event.payload.reason,
233
250
  });
234
- if (activePromptTurnId === event.payload.turnId) activePromptTurnId = null;
251
+ if (activePromptTurnId === event.payload.turnId) {
252
+ activePromptTurnId = null;
253
+ activePromptTurnText = null;
254
+ }
235
255
  }),
236
256
  );
237
257
  const {
@@ -270,6 +290,7 @@ export async function bootstrapRuntime(
270
290
  operatorPolicy: GOODVIBES_AGENT_OPERATOR_POLICY,
271
291
  shellPaths: services.shellPaths,
272
292
  memoryRegistry: services.memoryRegistry,
293
+ turnText: activePromptTurnText,
273
294
  });
274
295
  promptContextReceipts.record(composed.receipt);
275
296
  return composed.prompt;
@@ -482,6 +503,27 @@ export async function bootstrapRuntime(
482
503
  requestRender();
483
504
  },
484
505
  });
506
+ // W2A: probe the daemon spine OFF the interactive path. On a reachable daemon,
507
+ // fold the agent's own per-cwd legacy session store into it (once; marked
508
+ // migrated). Never starts the daemon; a down/absent daemon leaves the agent
509
+ // local-only with an honest offline status.
510
+ deferredStartup.schedule({
511
+ label: 'session-spine',
512
+ run: async () => {
513
+ const reachability = await services.sessionSpineClient.probeReachability();
514
+ if (reachability !== 'online') return;
515
+ foldLegacySpineStore(services.sessionSpineClient, {
516
+ storePath: services.shellPaths.resolveProjectPath(GOODVIBES_AGENT_SURFACE_ROOT, 'control-plane', 'sessions.json'),
517
+ markerPath: services.shellPaths.resolveProjectPath(GOODVIBES_AGENT_SURFACE_ROOT, 'control-plane', 'sessions.spine-folded.json'),
518
+ project: workingDir,
519
+ log: logger,
520
+ });
521
+ },
522
+ onError: (error) => {
523
+ logger.debug('Deferred session-spine startup failed', { error: summarizeError(error) });
524
+ },
525
+ });
526
+
485
527
  const toolCount = toolRegistry.list().length;
486
528
  conversation.splashOptions = {
487
529
  workingDir,
@@ -492,8 +534,14 @@ export async function bootstrapRuntime(
492
534
 
493
535
  // ── Phase 8: Background provider registration (non-blocking) ────────────
494
536
  // These run after the initial render so they don't delay startup.
537
+ //
538
+ // Gated: the SDK's discovery pass probes every host on the local subnet
539
+ // unconditionally, so it is only ever invoked through runGatedLanScan,
540
+ // which requires an explicit, persisted consent decision first and reframes
541
+ // whatever the SDK emits into a single honest summary line. See
542
+ // src/runtime/lan-scan-consent.ts.
495
543
 
496
- startBackgroundProviderRegistration({
544
+ runGatedLanScan({
497
545
  configManager,
498
546
  providerRegistry,
499
547
  runtime,
@@ -503,6 +551,17 @@ export async function bootstrapRuntime(
503
551
  shellPaths: services.shellPaths,
504
552
  surfaceRoot: GOODVIBES_AGENT_SURFACE_ROOT,
505
553
  });
554
+ // W4-A9: refresh explicitly-subscribed external calendar feeds that are DUE
555
+ // per their own bounded interval (conditional 304 requests keep it cheap).
556
+ // Consent was given at subscribe time ("now and on each refresh"); with no
557
+ // subscriptions this touches nothing. Non-blocking — boot never waits on it;
558
+ // one honest aggregate line only when something refreshed or failed.
559
+ void scheduleCalendarSubscriptionBootRefresh({
560
+ shellPaths: services.shellPaths,
561
+ secretsManager: services.secretsManager,
562
+ systemMessageRouter,
563
+ requestRender,
564
+ });
506
565
  const mcpDiscovery = scheduleBackgroundMcpDiscovery({
507
566
  mcpRegistry: services.mcpRegistry,
508
567
  systemMessageRouter,
@@ -597,6 +656,10 @@ export async function bootstrapRuntime(
597
656
  commandRegistry,
598
657
  systemMessageRouter,
599
658
  shutdown: async (sessionData) => {
659
+ // W2A: best-effort spine close (short timeout, fire-and-forget) then stop
660
+ // the heartbeat timer. Tolerates a racing daemon stop; never blocks teardown.
661
+ services.sessionSpineClient.close(runtime.sessionId);
662
+ services.sessionSpineClient.dispose();
600
663
  // Clear bootstrap-owned subscriptions
601
664
  bootstrapUnsubs.forEach(fn => fn());
602
665
  bootstrapUnsubs.length = 0;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Calendar subscription boot refresh (W4-A9 completion).
3
+ *
4
+ * Fires once from bootstrap's Phase-8 area (after first render, like the gated
5
+ * LAN scan and MCP discovery), fully async and non-blocking: boot never waits on
6
+ * the network. Consent was given at subscribe time — the subscribe flow states
7
+ * "this will fetch the calendar from the URL you provided, now and on each
8
+ * refresh" — so refreshing already-subscribed feeds at boot is the promised
9
+ * behavior, not a silent new fetch. Feeds that were never subscribed are never
10
+ * touched; when there are no subscriptions this function does nothing at all
11
+ * (no network, no store write, no activity line).
12
+ *
13
+ * Bounded: only feeds actually DUE per their per-subscription refresh interval
14
+ * are fetched (`force: false` — the SDK store skips the rest without network),
15
+ * and due fetches send stored etag/last-modified validators so an unchanged
16
+ * feed costs one conditional 304 round trip.
17
+ *
18
+ * Honest reporting: ONE aggregate activity line, emitted only when something
19
+ * was actually checked or failed — e.g.
20
+ * [Calendar] checked 2 subscriptions (1 updated); 'work' unreachable — will retry next refresh
21
+ * A conditional-GET 304 ("not-modified") means the feed WAS checked but had
22
+ * nothing new — F6 fix: that no longer counts toward "updated", so the line
23
+ * never claims something changed when the round trip only confirmed nothing
24
+ * did. An all-skipped (nothing due) boot stays silent. Failures keep the
25
+ * last-good cached events, and /calendar subscriptions still shows the honest
26
+ * per-feed health (unreachable / parse-error / stale with age).
27
+ */
28
+ import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
29
+ import type { ShellPathService } from '@/runtime/index.ts';
30
+ import {
31
+ CalendarSubscriptionRegistry,
32
+ createHttpFeedFetcher,
33
+ type RefreshOutcome,
34
+ type SubscriptionSecretStore,
35
+ } from '../agent/calendar-subscription-registry.ts';
36
+
37
+ interface SystemMessageSinkLike {
38
+ low(message: string): void;
39
+ }
40
+
41
+ export interface CalendarBootRefreshOptions {
42
+ readonly shellPaths: Pick<ShellPathService, 'resolveUserPath'>;
43
+ readonly secretsManager: SubscriptionSecretStore;
44
+ readonly systemMessageRouter: SystemMessageSinkLike;
45
+ readonly requestRender: () => void;
46
+ /** Injectable for tests — production callers omit it and get the real registry (real HTTP fetch). */
47
+ readonly buildRegistry?: () => CalendarSubscriptionRegistry;
48
+ }
49
+
50
+ /**
51
+ * Build the single aggregate line from refresh outcomes, or null when nothing
52
+ * user-visible happened (everything skipped as not-due).
53
+ *
54
+ * F6 fix: a 304-not-modified round trip is a genuine check (it counts toward
55
+ * "checked N subscriptions"), but it is NOT a change — only outcome ===
56
+ * 'updated' counts toward "(M updated)". The old wording ("refreshed N")
57
+ * counted both the same, so an all-304 boot ("nothing changed anywhere")
58
+ * still read as "refreshed 2 subscriptions", which claims an update that
59
+ * never happened.
60
+ */
61
+ export function formatCalendarBootRefreshLine(outcomes: readonly RefreshOutcome[]): string | null {
62
+ const checked = outcomes.filter((o) => o.outcome === 'updated' || o.outcome === 'not-modified');
63
+ const updated = checked.filter((o) => o.outcome === 'updated');
64
+ const failed = outcomes.filter((o) => o.outcome === 'unreachable' || o.outcome === 'parse-error');
65
+ if (checked.length === 0 && failed.length === 0) return null;
66
+
67
+ const parts: string[] = [];
68
+ if (checked.length > 0) {
69
+ parts.push(`checked ${checked.length} subscription${checked.length === 1 ? '' : 's'} (${updated.length} updated)`);
70
+ }
71
+ for (const f of failed) {
72
+ const label = f.outcome === 'unreachable' ? 'unreachable' : 'parse error';
73
+ parts.push(`'${f.name}' ${label} — will retry next refresh`);
74
+ }
75
+ return `[Calendar] ${parts.join('; ')}`;
76
+ }
77
+
78
+ /**
79
+ * Kick off the non-blocking boot refresh. Returns a promise that settles when
80
+ * the background pass finishes (bootstrap ignores it; tests await it). Never
81
+ * throws to the caller; failures are logged at debug and reported per-feed in
82
+ * the aggregate line.
83
+ */
84
+ export function scheduleCalendarSubscriptionBootRefresh(options: CalendarBootRefreshOptions): Promise<void> {
85
+ const run = async (): Promise<void> => {
86
+ try {
87
+ const registry = options.buildRegistry
88
+ ? options.buildRegistry()
89
+ : CalendarSubscriptionRegistry.create(options.shellPaths, options.secretsManager, createHttpFeedFetcher());
90
+ if (!registry.hasAny()) return; // nothing subscribed -> no network, no line
91
+
92
+ // force:false — only feeds due per their own bounded interval are fetched;
93
+ // the rest are skipped without touching the network.
94
+ const outcomes = await registry.refresh(undefined, { force: false });
95
+ const line = formatCalendarBootRefreshLine(outcomes);
96
+ if (line !== null) {
97
+ options.systemMessageRouter.low(line);
98
+ options.requestRender();
99
+ }
100
+ } catch (error) {
101
+ logger.debug('Calendar subscription boot refresh failed', { error: summarizeError(error) });
102
+ }
103
+ };
104
+ return run();
105
+ }