@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,11 +1,22 @@
1
- import type { MemoryRecord, MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
2
- const DEFAULT_LIMIT = 10;
3
- export const MIN_PROMPT_MEMORY_CONFIDENCE = 70;
1
+ import type { MemoryRecord, MemoryRegistry, MemoryVectorStats } from '@pellux/goodvibes-sdk/platform/state';
2
+ // W6-C2 (E6): the recall-honesty floor + eligibility receipt now live in the SDK as
3
+ // the ONE cross-surface contract (memory-recall-contract.ts) instead of being defined
4
+ // per surface. This file re-exports them unchanged so every existing agent consumer
5
+ // keeps importing from './memory-prompt.ts', while the SDK is the single source of the
6
+ // floor (60, the store's own baseline), the flagged-record exclusion, and the honest
7
+ // degraded-state distinction. The per-turn ranking below stays agent-local — it is
8
+ // injection wiring on top of the contract, not the contract itself.
9
+ import {
10
+ MIN_PROMPT_MEMORY_CONFIDENCE,
11
+ describeMemoryPromptEligibility,
12
+ isPromptActiveMemory,
13
+ } from '@pellux/goodvibes-sdk/platform/state';
14
+ import type { MemoryPromptEligibility } from '@pellux/goodvibes-sdk/platform/state';
4
15
 
5
- export function isPromptActiveMemory(record: MemoryRecord): boolean {
6
- // Fully autonomous learning: reviewState gate removed — confidence alone qualifies a memory for prompt recall.
7
- return record.confidence >= MIN_PROMPT_MEMORY_CONFIDENCE;
8
- }
16
+ export { MIN_PROMPT_MEMORY_CONFIDENCE, describeMemoryPromptEligibility, isPromptActiveMemory };
17
+ export type { MemoryPromptEligibility };
18
+
19
+ const DEFAULT_LIMIT = 10;
9
20
 
10
21
  function sortMemoryForPrompt(left: MemoryRecord, right: MemoryRecord): number {
11
22
  if (right.confidence !== left.confidence) return right.confidence - left.confidence;
@@ -20,11 +31,131 @@ function formatMemoryLine(record: MemoryRecord): string {
20
31
  return `- [${record.scope}/${record.cls} ${record.confidence}%${tags}${provenance}] ${record.summary}`;
21
32
  }
22
33
 
23
- export function buildReviewedMemoryPrompt(memoryRegistry: MemoryRegistry, limit = DEFAULT_LIMIT): string | null {
24
- const records = memoryRegistry.getAll()
25
- .filter(isPromptActiveMemory)
26
- .sort(sortMemoryForPrompt)
27
- .slice(0, Math.max(0, limit));
34
+ /**
35
+ * Per-turn semantic ranking of an already-eligible memory set (Wave-4 W4-A1B).
36
+ *
37
+ * W4-A1 shipped eligibility (confidence + reviewState + provenance) as the hard trust
38
+ * gate but had no per-turn query to rank WITHIN that eligible set — records were only
39
+ * ever ordered by stored confidence/recency, regardless of whether they had anything to
40
+ * do with what the user actually just asked. `rankMemoryForTurn` never touches the gate
41
+ * itself: it only reorders the records that already cleared describeMemoryPromptEligibility,
42
+ * so a budget-limited cut (the top-N prompt slice) drops the least relevant record to
43
+ * THIS turn instead of an arbitrary one.
44
+ *
45
+ * Degrades honestly and says why whenever it can't score: no turn text supplied, the
46
+ * semantic index disabled/unavailable/empty, or a real query that the index has zero
47
+ * vector matches for. In every degraded case the eligible set still gets the prior
48
+ * confidence/recency order — never silently dropped, never silently reordered on data
49
+ * that isn't there.
50
+ */
51
+ export interface MemoryTurnRankingResult {
52
+ /** The eligible records, ranked — by relevance when `scored` is true, else by the prior confidence/recency order. */
53
+ readonly records: readonly MemoryRecord[];
54
+ /** Relevance percent (0-100) per record id. Only populated when `scored` is true. */
55
+ readonly relevanceById: ReadonlyMap<string, number>;
56
+ /** True when semantic relevance to the current turn actually drove the ranking above. */
57
+ readonly scored: boolean;
58
+ /** Honest reason for the degrade when `scored` is false; null when scoring succeeded or there was nothing to rank. */
59
+ readonly degradedReason: string | null;
60
+ }
61
+
62
+ function describeTurnRelevanceIndexUnavailable(stats: MemoryVectorStats): string | null {
63
+ if (!stats.enabled) return 'the semantic memory index is disabled for this store';
64
+ if (!stats.available) return `the semantic memory index is unavailable${stats.error ? `: ${stats.error}` : ''}`;
65
+ if (stats.indexedRecords === 0) return 'the semantic memory index has no indexed records yet';
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Qualitative band for a 0-100 relevance-to-turn percent (F7a).
71
+ *
72
+ * A raw cosine-similarity percent like "28%" reads as misleadingly low out of
73
+ * context — it can still be the single best match among the eligible set, or
74
+ * a genuinely useful match on an absolute basis, but a bare number invites
75
+ * "only 28%? that's barely relevant." Choice made here: add a qualitative
76
+ * band NEXT TO the raw percent rather than normalizing against the turn's
77
+ * top score. Normalizing would make every record but the single best one
78
+ * read as comparatively weak even when several are strong matches in
79
+ * absolute terms, and would make a genuinely weak top match look like a
80
+ * confident 100%. A fixed absolute band keeps the number honest while
81
+ * making it readable at a glance.
82
+ */
83
+ export function relevanceBand(percent: number): 'high match' | 'moderate match' | 'low match' {
84
+ if (percent >= 60) return 'high match';
85
+ if (percent >= 35) return 'moderate match';
86
+ return 'low match';
87
+ }
88
+
89
+ export function rankMemoryForTurn(
90
+ memoryRegistry: MemoryRegistry,
91
+ eligible: readonly MemoryRecord[],
92
+ turnText: string | null | undefined,
93
+ ): MemoryTurnRankingResult {
94
+ const fallbackOrder = (): readonly MemoryRecord[] => [...eligible].sort(sortMemoryForPrompt);
95
+ if (eligible.length === 0) {
96
+ return { records: [], relevanceById: new Map(), scored: false, degradedReason: null };
97
+ }
98
+ const trimmedTurnText = turnText?.trim() ?? '';
99
+ if (!trimmedTurnText) {
100
+ return {
101
+ records: fallbackOrder(),
102
+ relevanceById: new Map(),
103
+ scored: false,
104
+ degradedReason: 'no current-turn text available for this composition — using stored confidence/recency order',
105
+ };
106
+ }
107
+ const stats = memoryRegistry.vectorStats();
108
+ const indexUnavailable = describeTurnRelevanceIndexUnavailable(stats);
109
+ if (indexUnavailable) {
110
+ return {
111
+ records: fallbackOrder(),
112
+ relevanceById: new Map(),
113
+ scored: false,
114
+ degradedReason: `semantic index unavailable (${indexUnavailable}) — using stored confidence/recency order`,
115
+ };
116
+ }
117
+ // Request enough candidates back that every eligible record has a real chance to
118
+ // appear with its similarity score (searchSemantic slices its return to this limit
119
+ // after scoring, not before) — the SDK's vector store caps the underlying KNN search
120
+ // at 500 candidates regardless, so there is no point asking for more than that.
121
+ const requestLimit = Math.min(500, Math.max(eligible.length, 50));
122
+ const results = memoryRegistry.searchSemantic({ query: trimmedTurnText, limit: requestLimit });
123
+ const consultedSemanticIndex = results.some((entry) => entry.similarity > 0);
124
+ if (!consultedSemanticIndex) {
125
+ return {
126
+ records: fallbackOrder(),
127
+ relevanceById: new Map(),
128
+ scored: false,
129
+ degradedReason: "no semantic match for this turn's text — using stored confidence/recency order",
130
+ };
131
+ }
132
+ const eligibleIds = new Set(eligible.map((record) => record.id));
133
+ const relevanceById = new Map<string, number>();
134
+ for (const entry of results) {
135
+ if (!eligibleIds.has(entry.record.id)) continue;
136
+ relevanceById.set(entry.record.id, Math.round(Math.max(0, Math.min(1, entry.similarity)) * 100));
137
+ }
138
+ const ranked = [...eligible].sort((left, right) => {
139
+ const rightScore = relevanceById.get(right.id) ?? 0;
140
+ const leftScore = relevanceById.get(left.id) ?? 0;
141
+ if (rightScore !== leftScore) return rightScore - leftScore;
142
+ return sortMemoryForPrompt(left, right);
143
+ });
144
+ return { records: ranked, relevanceById, scored: true, degradedReason: null };
145
+ }
146
+
147
+ export interface BuildReviewedMemoryPromptOptions {
148
+ readonly limit?: number;
149
+ /** The current turn's raw text (the seam this comes from: TURN_SUBMITTED's `prompt`).
150
+ * Used only to RANK the already-eligible set — never to admit an otherwise-ineligible record. */
151
+ readonly turnText?: string | null;
152
+ }
153
+
154
+ export function buildReviewedMemoryPrompt(memoryRegistry: MemoryRegistry, options: BuildReviewedMemoryPromptOptions = {}): string | null {
155
+ const limit = options.limit ?? DEFAULT_LIMIT;
156
+ const eligible = memoryRegistry.getAll().filter(isPromptActiveMemory);
157
+ const ranking = rankMemoryForTurn(memoryRegistry, eligible, options.turnText);
158
+ const records = ranking.records.slice(0, Math.max(0, limit));
28
159
 
29
160
  if (records.length === 0) return null;
30
161
  return [
@@ -6,14 +6,14 @@ export type JsonRecord = Record<string, unknown>;
6
6
  export type ApprovalActionId = 'approvals.approve' | 'approvals.deny' | 'approvals.cancel';
7
7
  export type AutomationActionId =
8
8
  | 'automation.jobs.run'
9
- | 'automation.jobs.pause'
10
- | 'automation.jobs.resume'
9
+ | 'automation.jobs.disable'
10
+ | 'automation.jobs.enable'
11
11
  | 'automation.runs.cancel'
12
12
  | 'automation.runs.retry'
13
- | 'schedules.delete'
14
- | 'schedules.disable'
15
- | 'schedules.enable'
16
- | 'schedules.run';
13
+ | 'automation.schedules.delete'
14
+ | 'automation.schedules.disable'
15
+ | 'automation.schedules.enable'
16
+ | 'automation.schedules.run';
17
17
  export type OperatorActionId = ApprovalActionId | AutomationActionId;
18
18
  export type OperatorActionHttpMethod = 'POST' | 'DELETE';
19
19
 
@@ -103,16 +103,20 @@ export const OPERATOR_ACTIONS: Record<OperatorActionId, OperatorActionDescriptor
103
103
  pathTemplate: '/api/automation/jobs/{jobId}/run',
104
104
  targetField: 'jobId',
105
105
  },
106
- 'automation.jobs.pause': {
107
- action: 'automation.jobs.pause',
106
+ // W6-C3: automation.jobs.pause/resume were retired (redundant with
107
+ // disable/enable — same {id,enabled} output, same semantics). The
108
+ // user-facing "pause"/"resume" verb is unchanged; only the wire action +
109
+ // path moved to the canonical disable/enable methods.
110
+ 'automation.jobs.disable': {
111
+ action: 'automation.jobs.disable',
108
112
  label: 'pause automation job',
109
- pathTemplate: '/api/automation/jobs/{jobId}/pause',
113
+ pathTemplate: '/api/automation/jobs/{jobId}/disable',
110
114
  targetField: 'jobId',
111
115
  },
112
- 'automation.jobs.resume': {
113
- action: 'automation.jobs.resume',
116
+ 'automation.jobs.enable': {
117
+ action: 'automation.jobs.enable',
114
118
  label: 'resume automation job',
115
- pathTemplate: '/api/automation/jobs/{jobId}/resume',
119
+ pathTemplate: '/api/automation/jobs/{jobId}/enable',
116
120
  targetField: 'jobId',
117
121
  },
118
122
  'automation.runs.cancel': {
@@ -127,26 +131,26 @@ export const OPERATOR_ACTIONS: Record<OperatorActionId, OperatorActionDescriptor
127
131
  pathTemplate: '/api/automation/runs/{runId}/retry',
128
132
  targetField: 'runId',
129
133
  },
130
- 'schedules.run': {
131
- action: 'schedules.run',
134
+ 'automation.schedules.run': {
135
+ action: 'automation.schedules.run',
132
136
  label: 'run schedule',
133
137
  pathTemplate: '/api/automation/schedules/{scheduleId}/run',
134
138
  targetField: 'scheduleId',
135
139
  },
136
- 'schedules.enable': {
137
- action: 'schedules.enable',
140
+ 'automation.schedules.enable': {
141
+ action: 'automation.schedules.enable',
138
142
  label: 'enable schedule',
139
143
  pathTemplate: '/api/automation/schedules/{scheduleId}/enable',
140
144
  targetField: 'scheduleId',
141
145
  },
142
- 'schedules.disable': {
143
- action: 'schedules.disable',
146
+ 'automation.schedules.disable': {
147
+ action: 'automation.schedules.disable',
144
148
  label: 'disable schedule',
145
149
  pathTemplate: '/api/automation/schedules/{scheduleId}/disable',
146
150
  targetField: 'scheduleId',
147
151
  },
148
- 'schedules.delete': {
149
- action: 'schedules.delete',
152
+ 'automation.schedules.delete': {
153
+ action: 'automation.schedules.delete',
150
154
  label: 'delete schedule',
151
155
  pathTemplate: '/api/automation/schedules/{scheduleId}',
152
156
  targetField: 'scheduleId',
@@ -340,14 +344,14 @@ function scheduleIdFromActionPath(path: string): string {
340
344
  }
341
345
 
342
346
  export function formatOperatorActionSuccess(baseUrl: string, result: OperatorActionSuccess): string {
343
- const scheduleId = result.methodId.startsWith('schedules.') ? scheduleIdFromActionPath(result.path) : '';
347
+ const scheduleId = result.methodId.startsWith('automation.schedules.') ? scheduleIdFromActionPath(result.path) : '';
344
348
  return [
345
349
  'Agent operator action completed',
346
350
  ` method: ${result.methodId}`,
347
351
  ` route: ${result.httpMethod} ${result.path}`,
348
352
  ` connected host: ${baseUrl}`,
349
353
  ` status: ${statusFromOperatorActionBody(result.body)}`,
350
- ...(scheduleId ? scheduleNextRouteLines(scheduleId, { deleted: result.methodId === 'schedules.delete' }) : []),
354
+ ...(scheduleId ? scheduleNextRouteLines(scheduleId, { deleted: result.methodId === 'automation.schedules.delete' }) : []),
351
355
  ].join('\n');
352
356
  }
353
357
 
@@ -1,10 +1,10 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { dirname } from 'node:path';
4
- import type { MemoryRecord, MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
4
+ import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
5
5
  import { getTierForContextWindow, getTierPromptSupplement } from '@pellux/goodvibes-sdk/platform/providers';
6
6
  import type { ShellPathService } from '@/runtime/index.ts';
7
- import { buildReviewedMemoryPrompt, isPromptActiveMemory, MIN_PROMPT_MEMORY_CONFIDENCE } from './memory-prompt.ts';
7
+ import { buildReviewedMemoryPrompt, describeMemoryPromptEligibility, isPromptActiveMemory, rankMemoryForTurn, relevanceBand } from './memory-prompt.ts';
8
8
  import { AgentPersonaRegistry, buildActivePersonaPrompt } from './persona-registry.ts';
9
9
  import { buildProjectContextPrompt, discoverProjectContextFiles } from './project-context-files.ts';
10
10
  import { AgentRoutineRegistry, buildEnabledRoutinesPrompt, evaluateAgentRoutineReadiness } from './routine-registry.ts';
@@ -73,6 +73,10 @@ export interface RuntimePromptCompositionInput {
73
73
  readonly operatorPolicy: string;
74
74
  readonly shellPaths: ShellPathService;
75
75
  readonly memoryRegistry: MemoryRegistry;
76
+ /** The active turn's raw text (TURN_SUBMITTED's `prompt`), used only to rank the
77
+ * already-eligible memory set by relevance to this turn — see rankMemoryForTurn.
78
+ * Null/undefined when there is no active turn (e.g. a follow-up composition). */
79
+ readonly turnText?: string | null;
76
80
  }
77
81
 
78
82
  const RECEIPT_STORE_LIMIT = 200;
@@ -117,11 +121,6 @@ function statusFor(active: number, suppressed: number): PromptContextReceiptStat
117
121
  return 'empty';
118
122
  }
119
123
 
120
- function memorySort(left: MemoryRecord, right: MemoryRecord): number {
121
- if (right.confidence !== left.confidence) return right.confidence - left.confidence;
122
- return right.updatedAt - left.updatedAt;
123
- }
124
-
125
124
  function receiptSegment(input: Omit<PromptContextReceiptSegment, 'approxTokens'> & { readonly promptText?: string }): PromptContextReceiptSegment {
126
125
  const promptChars = input.promptText?.length ?? input.promptChars;
127
126
  return {
@@ -145,10 +144,12 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
145
144
  const projectContext = discoverProjectContextFiles(input.shellPaths);
146
145
  const projectContextPrompt = buildProjectContextPrompt(input.shellPaths) ?? '';
147
146
  const memoryRecords = input.memoryRegistry.getAll();
148
- const activeMemory = memoryRecords.filter(isPromptActiveMemory).sort(memorySort).slice(0, 10);
147
+ const eligibleMemory = memoryRecords.filter(isPromptActiveMemory);
148
+ const memoryRanking = rankMemoryForTurn(input.memoryRegistry, eligibleMemory, input.turnText);
149
+ const activeMemory = memoryRanking.records.slice(0, 10);
149
150
  const activeMemoryIds = new Set(activeMemory.map((record) => record.id));
150
151
  const suppressedMemory = memoryRecords.filter((record) => !activeMemoryIds.has(record.id));
151
- const memoryPrompt = buildReviewedMemoryPrompt(input.memoryRegistry) ?? '';
152
+ const memoryPrompt = buildReviewedMemoryPrompt(input.memoryRegistry, { turnText: input.turnText }) ?? '';
152
153
  const routineSnapshot = AgentRoutineRegistry.fromShellPaths(input.shellPaths).snapshot();
153
154
  const activeRoutines = routineSnapshot.enabledRoutines.filter((routine) => routine.reviewState === 'reviewed' && evaluateAgentRoutineReadiness(routine).ready);
154
155
  const suppressedRoutines = routineSnapshot.enabledRoutines.filter((routine) => !activeRoutines.some((active) => active.id === routine.id));
@@ -231,17 +232,43 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
231
232
  suppressedCount: suppressedMemory.length,
232
233
  promptChars: memoryPrompt.length,
233
234
  promptText: memoryPrompt,
234
- selected: activeMemory.map((record) => ({ id: record.id, scope: record.scope, class: record.cls, confidence: record.confidence })),
235
- suppressed: suppressedMemory.slice(0, 12).map((record) => ({
235
+ // Honest degrade note (Wave-4 W4-A1B): when per-turn relevance scoring did not
236
+ // run — no active-turn text, semantic index unavailable, or no vector match —
237
+ // say so instead of silently presenting the fallback confidence/recency order as
238
+ // if it were a relevance ranking.
239
+ note: memoryRanking.scored ? undefined : (memoryRanking.degradedReason ?? undefined),
240
+ selected: activeMemory.map((record) => ({
236
241
  id: record.id,
237
- reviewState: record.reviewState,
242
+ scope: record.scope,
243
+ class: record.cls,
238
244
  confidence: record.confidence,
239
- reason: record.reviewState !== 'reviewed'
240
- ? 'not reviewed'
241
- : record.confidence < MIN_PROMPT_MEMORY_CONFIDENCE
242
- ? `confidence below ${MIN_PROMPT_MEMORY_CONFIDENCE}`
243
- : 'outside prompt limit',
245
+ reason: describeMemoryPromptEligibility(record).reason,
246
+ // Per-turn relevance (W4-A1B): honest wording, only present when actually scored.
247
+ // F7a: the raw percent is paired with a qualitative band (see relevanceBand)
248
+ // so a genuinely-lower-but-real score like "28%" doesn't read as noise.
249
+ ...(memoryRanking.scored ? { relevance: `relevance to this turn: ${memoryRanking.relevanceById.get(record.id) ?? 0}% (${relevanceBand(memoryRanking.relevanceById.get(record.id) ?? 0)})` } : {}),
244
250
  })),
251
+ suppressed: suppressedMemory.slice(0, 12).map((record) => {
252
+ // Honest, per-record reason (confidence + reviewState + provenance) — never a
253
+ // blanket "not reviewed"/"outside prompt limit" guess. A record lands in
254
+ // "suppressed" either because it genuinely failed eligibility, or because it
255
+ // passed eligibility but the top-10 prompt slice cut it off — those are
256
+ // different situations and must read differently. When the slice was
257
+ // relevance-ranked, say what this record's relevance to the turn was too, so a
258
+ // budget-limited cut reads as "less relevant than the other ten", not arbitrary.
259
+ const eligibility = describeMemoryPromptEligibility(record);
260
+ const relevanceSuffix = memoryRanking.scored
261
+ ? ` (relevance to this turn: ${memoryRanking.relevanceById.get(record.id) ?? 0}% — ${relevanceBand(memoryRanking.relevanceById.get(record.id) ?? 0)})`
262
+ : '';
263
+ return {
264
+ id: record.id,
265
+ reviewState: record.reviewState,
266
+ confidence: record.confidence,
267
+ reason: eligibility.eligible
268
+ ? `eligible (${eligibility.reason}) but outside the top-10 prompt slice${relevanceSuffix} — budget-limited, not a trust problem`
269
+ : eligibility.reason,
270
+ };
271
+ }),
245
272
  }),
246
273
  receiptSegment({
247
274
  id: 'routines',
@@ -317,7 +344,7 @@ export function composeRuntimePromptWithReceipt(input: RuntimePromptCompositionI
317
344
  buildVibePrompt(input.shellPaths),
318
345
  buildProjectContextPrompt(input.shellPaths),
319
346
  input.operatorPolicy,
320
- buildReviewedMemoryPrompt(input.memoryRegistry),
347
+ buildReviewedMemoryPrompt(input.memoryRegistry, { turnText: input.turnText }),
321
348
  buildEnabledRoutinesPrompt(input.shellPaths),
322
349
  buildEnabledSkillsPrompt(input.shellPaths),
323
350
  buildActivePersonaPrompt(input.shellPaths),
@@ -89,7 +89,7 @@ export function formatReminderScheduleFailure(failure: ReminderScheduleFailure):
89
89
  ? ' next make the connected GoodVibes host available outside Agent, then retry.'
90
90
  : null,
91
91
  failure.kind === 'connected_host_incompatible' || failure.kind === 'connected_host_route_unavailable'
92
- ? ' next update the connected GoodVibes host so public schedules.create is available.'
92
+ ? ' next update the connected GoodVibes host so public automation.schedules.create is available.'
93
93
  : null,
94
94
  ].filter((line): line is string => Boolean(line)).join('\n');
95
95
  }
@@ -15,8 +15,8 @@ import {
15
15
  type RoutineScheduleSpec,
16
16
  } from './routine-schedule-promotion.ts';
17
17
 
18
- type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
19
- type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
18
+ type ScheduleCreateInput = OperatorMethodInput<'automation.schedules.create'>;
19
+ type ScheduleCreateOutput = OperatorMethodOutput<'automation.schedules.create'>;
20
20
  type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
21
21
  type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
22
22
 
@@ -441,7 +441,7 @@ async function classifyReminderScheduleError(
441
441
  return {
442
442
  ok: false,
443
443
  kind: 'connected_host_incompatible',
444
- error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; schedules.create is unavailable.',
444
+ error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; automation.schedules.create is unavailable.',
445
445
  route: REMINDER_SCHEDULE_ROUTE,
446
446
  baseUrl: connection.baseUrl,
447
447
  };
@@ -166,7 +166,7 @@ export function formatRoutineScheduleCorrelation(result: RoutineScheduleCorrelat
166
166
  ? ' next make the connected GoodVibes host available outside Agent, then retry.'
167
167
  : null,
168
168
  result.kind === 'connected_host_incompatible' || result.kind === 'connected_host_route_unavailable'
169
- ? ' next update the connected GoodVibes host so public schedules.list is available.'
169
+ ? ' next update the connected GoodVibes host so public automation.schedules.list is available.'
170
170
  : null,
171
171
  ].filter((line): line is string => Boolean(line)).join('\n');
172
172
  }
@@ -218,7 +218,7 @@ export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFa
218
218
  ? ' next make the connected GoodVibes host available outside Agent, then retry.'
219
219
  : null,
220
220
  failure.kind === 'connected_host_incompatible' || failure.kind === 'connected_host_route_unavailable'
221
- ? ' next update the connected GoodVibes host so public schedules.create is available.'
221
+ ? ' next update the connected GoodVibes host so public automation.schedules.create is available.'
222
222
  : null,
223
223
  ].filter((line): line is string => Boolean(line)).join('\n');
224
224
  }
@@ -8,11 +8,11 @@ import { formatAgentRecordReviewState } from './record-labels.ts';
8
8
  import type { AgentRoutineRecord } from './routine-registry.ts';
9
9
 
10
10
  export const ROUTINE_SCHEDULE_ROUTE = '/api/automation/schedules';
11
- export const ROUTINE_SCHEDULE_METHOD = 'schedules.create';
12
- export const ROUTINE_SCHEDULE_LIST_METHOD = 'schedules.list';
11
+ export const ROUTINE_SCHEDULE_METHOD = 'automation.schedules.create';
12
+ export const ROUTINE_SCHEDULE_LIST_METHOD = 'automation.schedules.list';
13
13
 
14
- type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
15
- type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
14
+ type ScheduleCreateInput = OperatorMethodInput<'automation.schedules.create'>;
15
+ type ScheduleCreateOutput = OperatorMethodOutput<'automation.schedules.create'>;
16
16
  type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
17
17
  type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
18
18
 
@@ -388,7 +388,7 @@ async function classifyScheduleError(
388
388
  return {
389
389
  ok: false,
390
390
  kind: 'connected_host_incompatible',
391
- error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; schedules.create is unavailable.',
391
+ error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; automation.schedules.create is unavailable.',
392
392
  route: ROUTINE_SCHEDULE_ROUTE,
393
393
  baseUrl: connection.baseUrl,
394
394
  };
@@ -28,7 +28,7 @@ import {
28
28
 
29
29
  type ScheduleCreateInput = RoutineSchedulePromotionPreview['payload'];
30
30
  type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
31
- type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
31
+ type ScheduleListOutput = OperatorMethodOutput<'automation.schedules.list'>;
32
32
 
33
33
  interface RoutineScheduleReceiptStoreFile {
34
34
  readonly version: 1;
@@ -453,7 +453,7 @@ async function classifyScheduleListError(
453
453
  return {
454
454
  ok: false,
455
455
  kind: 'connected_host_incompatible',
456
- error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; schedules.list is unavailable.',
456
+ error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule requirements; automation.schedules.list is unavailable.',
457
457
  route: ROUTINE_SCHEDULE_ROUTE,
458
458
  baseUrl: connection.baseUrl,
459
459
  };
@@ -123,7 +123,7 @@ export function formatScheduleEditFailure(failure: ScheduleEditFailure): string
123
123
  ? ' next pair or start the connected GoodVibes host so an operator token is available.'
124
124
  : '',
125
125
  failure.kind === 'connected_host_incompatible'
126
- ? ' next update the connected GoodVibes host so automation.jobs.patch is available.'
126
+ ? ' next update the connected GoodVibes host so automation.jobs.update is available.'
127
127
  : '',
128
128
  ].filter((line) => line !== '').join('\n');
129
129
  }
@@ -21,11 +21,11 @@ import {
21
21
  type RoutineScheduleSpec,
22
22
  } from './routine-schedule-promotion.ts';
23
23
 
24
- type SchedulePatchInput = OperatorMethodInput<'automation.jobs.patch'>;
25
- type SchedulePatchOutput = OperatorMethodOutput<'automation.jobs.patch'>;
26
- type ScheduleListOutput = OperatorMethodOutput<'schedules.list'>;
24
+ type SchedulePatchInput = OperatorMethodInput<'automation.jobs.update'>;
25
+ type SchedulePatchOutput = OperatorMethodOutput<'automation.jobs.update'>;
26
+ type ScheduleListOutput = OperatorMethodOutput<'automation.schedules.list'>;
27
27
 
28
- export const SCHEDULE_EDIT_METHOD = 'automation.jobs.patch';
28
+ export const SCHEDULE_EDIT_METHOD = 'automation.jobs.update';
29
29
  export const SCHEDULE_EDIT_ROUTE = '/api/automation/jobs/{jobId}';
30
30
 
31
31
  export interface ParsedScheduleEditArgs {
@@ -442,7 +442,7 @@ async function classifyScheduleEditError(
442
442
  return {
443
443
  ok: false,
444
444
  kind: 'connected_host_incompatible',
445
- error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule edit requirements; automation.jobs.patch is unavailable.',
445
+ error: 'Connected GoodVibes host compatibility does not satisfy Agent schedule edit requirements; automation.jobs.update is unavailable.',
446
446
  route: SCHEDULE_EDIT_ROUTE,
447
447
  baseUrl: connection.baseUrl,
448
448
  };