@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,7 +1,7 @@
1
1
  import type { MemoryApi } from '@pellux/goodvibes-sdk/platform/knowledge';
2
2
  import { getTierForContextWindow, getTierPromptSupplement } from '@pellux/goodvibes-sdk/platform/providers';
3
3
  import type { MemoryRecord } from '@pellux/goodvibes-sdk/platform/state';
4
- import { isPromptActiveMemory, MIN_PROMPT_MEMORY_CONFIDENCE } from '../agent/memory-prompt.ts';
4
+ import { describeMemoryPromptEligibility, isPromptActiveMemory, MIN_PROMPT_MEMORY_CONFIDENCE } from '../agent/memory-prompt.ts';
5
5
  import { AgentPersonaRegistry, buildActivePersonaPrompt } from '../agent/persona-registry.ts';
6
6
  import { buildProjectContextPrompt, discoverProjectContextFiles } from '../agent/project-context-files.ts';
7
7
  import { AgentRoutineRegistry, buildEnabledRoutinesPrompt, evaluateAgentRoutineReadiness } from '../agent/routine-registry.ts';
@@ -229,6 +229,14 @@ function memorySegment(memory: PromptMemoryApi | undefined, includeParameters: b
229
229
  suppressedCount: suppressed.length,
230
230
  promptChars: promptText.length,
231
231
  promptText,
232
+ // F7c: this segment orders the eligible set by stored confidence/recency
233
+ // (memorySort), NOT by relevance to the current turn. The `context
234
+ // action:"receipt"` surface (prompt-context-receipts.ts) additionally
235
+ // ranks the same eligible set by per-turn semantic relevance when that
236
+ // scoring succeeds. Stating the ordering basis here up front keeps the
237
+ // two memory surfaces from reading as contradictory when their orders
238
+ // differ for the same eligible records.
239
+ note: 'Ordered by stored confidence/recency, not by relevance to the current turn — see context action:"receipt" for per-turn relevance scoring.',
232
240
  route: 'memory action:"status"',
233
241
  selected: active.map((record) => ({
234
242
  id: record.id,
@@ -237,17 +245,23 @@ function memorySegment(memory: PromptMemoryApi | undefined, includeParameters: b
237
245
  confidence: record.confidence,
238
246
  inspectRoute: `agent_local_registry domain:"memory" action:"get" recordId:"${record.id}"`,
239
247
  })),
240
- suppressed: suppressed.slice(0, 12).map((record) => ({
241
- id: record.id,
242
- reviewState: record.reviewState,
243
- confidence: record.confidence,
244
- reason: record.reviewState !== 'reviewed'
245
- ? 'not reviewed'
246
- : record.confidence < MIN_PROMPT_MEMORY_CONFIDENCE
247
- ? `confidence below ${MIN_PROMPT_MEMORY_CONFIDENCE}`
248
- : 'outside prompt limit',
249
- inspectRoute: `agent_local_registry domain:"memory" action:"get" recordId:"${record.id}"`,
250
- })),
248
+ suppressed: suppressed.slice(0, 12).map((record) => {
249
+ // Honest, per-record reason straight from describeMemoryPromptEligibility (Wave-4
250
+ // W4-A1B) — the same wording source prompt-context-receipts.ts uses, never a
251
+ // locally invented "not reviewed"/"outside prompt limit" guess. A record here
252
+ // either genuinely failed eligibility, or passed it but was cut by the top-10
253
+ // prompt slice — those read differently.
254
+ const eligibility = describeMemoryPromptEligibility(record);
255
+ return {
256
+ id: record.id,
257
+ reviewState: record.reviewState,
258
+ confidence: record.confidence,
259
+ reason: eligibility.eligible
260
+ ? `eligible (${eligibility.reason}) but outside the top-10 prompt slice — budget-limited, not a trust problem`
261
+ : eligibility.reason,
262
+ inspectRoute: `agent_local_registry domain:"memory" action:"get" recordId:"${record.id}"`,
263
+ };
264
+ }),
251
265
  }, includeParameters);
252
266
  }
253
267
 
@@ -29,6 +29,7 @@ export interface WorkspaceEditorContext {
29
29
  readonly selectedRoutine: AgentWorkspaceLocalLibraryItem | null;
30
30
  readonly recentReviewerHandoffArtifacts: AgentWorkspaceRuntimeSnapshot['recentReviewerHandoffArtifacts'];
31
31
  readonly reviewPacketDefaults: AgentWorkspaceRuntimeSnapshot['reviewPacketDefaults'] | null;
32
+ readonly calendarOAuthConfigStatus: AgentWorkspaceRuntimeSnapshot['calendarOAuthConfigStatus'] | null;
32
33
  }
33
34
 
34
35
  export interface WorkspaceActionLookup {
@@ -247,6 +248,7 @@ export function buildWorkspaceEditorContext(context: CommandContext, args: Agent
247
248
  selectedRoutine: selectedRoutineFromArgs(snapshot, args),
248
249
  recentReviewerHandoffArtifacts: snapshot.recentReviewerHandoffArtifacts,
249
250
  reviewPacketDefaults: snapshot.reviewPacketDefaults,
251
+ calendarOAuthConfigStatus: snapshot.calendarOAuthConfigStatus,
250
252
  };
251
253
  } catch {
252
254
  return {
@@ -254,6 +256,7 @@ export function buildWorkspaceEditorContext(context: CommandContext, args: Agent
254
256
  selectedRoutine: null,
255
257
  recentReviewerHandoffArtifacts: [],
256
258
  reviewPacketDefaults: null,
259
+ calendarOAuthConfigStatus: null,
257
260
  };
258
261
  }
259
262
  }
@@ -267,6 +270,7 @@ export function createWorkspaceEditor(
267
270
  selectedRoutine: editorKind === 'routine-schedule' ? editorContext?.selectedRoutine ?? null : null,
268
271
  recentReviewerHandoffArtifacts: editorContext?.recentReviewerHandoffArtifacts ?? [],
269
272
  reviewPacketDefaults: editorContext?.reviewPacketDefaults ?? null,
273
+ calendarOAuthConfigStatus: editorContext?.calendarOAuthConfigStatus ?? null,
270
274
  });
271
275
  }
272
276
 
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Shared arg-reading/validation helpers for the agent_local_registry tool.
3
+ *
4
+ * Split out of agent-local-registry-tool.ts to stay under the 800-line
5
+ * architecture cap. These are generic across every domain (memory, note,
6
+ * persona, skill, skill_bundle, routine) — no domain-specific logic lives
7
+ * here.
8
+ */
9
+
10
+ export interface AgentLocalRegistryToolArgs {
11
+ readonly domain?: unknown;
12
+ readonly action?: unknown;
13
+ readonly id?: unknown;
14
+ readonly query?: unknown;
15
+ readonly semantic?: unknown;
16
+ readonly cls?: unknown;
17
+ readonly scope?: unknown;
18
+ readonly summary?: unknown;
19
+ readonly detail?: unknown;
20
+ readonly confidence?: unknown;
21
+ readonly title?: unknown;
22
+ readonly name?: unknown;
23
+ readonly description?: unknown;
24
+ readonly body?: unknown;
25
+ readonly sourceUrl?: unknown;
26
+ readonly procedure?: unknown;
27
+ readonly steps?: unknown;
28
+ readonly skills?: unknown;
29
+ readonly skillIds?: unknown;
30
+ readonly requiresEnv?: unknown;
31
+ readonly requiresCommands?: unknown;
32
+ readonly triggers?: unknown;
33
+ readonly tags?: unknown;
34
+ readonly reason?: unknown;
35
+ readonly enabled?: unknown;
36
+ readonly activate?: unknown;
37
+ readonly provenance?: unknown;
38
+ readonly confirm?: unknown;
39
+ readonly explicitUserRequest?: unknown;
40
+ }
41
+
42
+ export const AGENT_TOOL_PROVENANCE = 'agent-local-registry-tool';
43
+
44
+ export function readString(value: unknown): string {
45
+ return typeof value === 'string' ? value.trim() : '';
46
+ }
47
+
48
+ export function readAffirmative(value: unknown): boolean {
49
+ const normalized = readString(value).toLowerCase();
50
+ return value === true || (typeof value === 'string' && (normalized === '' || normalized === 'yes' || normalized === 'y' || normalized === 'true' || normalized === 'on'));
51
+ }
52
+
53
+ export function readStringList(value: unknown): readonly string[] {
54
+ if (typeof value === 'string') {
55
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
56
+ }
57
+ if (!Array.isArray(value)) return [];
58
+ return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
59
+ }
60
+
61
+ export function readOptionalNumber(value: unknown): number | undefined {
62
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
63
+ if (typeof value !== 'string' || !value.trim()) return undefined;
64
+ const parsed = Number(value);
65
+ return Number.isFinite(parsed) ? parsed : undefined;
66
+ }
67
+
68
+ export function readOptionalConfidence(value: unknown): number | undefined {
69
+ const confidence = readOptionalNumber(value);
70
+ if (confidence === undefined) return undefined;
71
+ if (confidence < 0 || confidence > 100) throw new Error('confidence must be between 0 and 100.');
72
+ return confidence;
73
+ }
74
+
75
+ export function registryError(message: string): { readonly success: false; readonly error: string } {
76
+ return { success: false, error: message };
77
+ }
78
+
79
+ export function registryOutput(output: string): { readonly success: true; readonly output: string } {
80
+ return { success: true, output };
81
+ }
82
+
83
+ export function requireId(args: AgentLocalRegistryToolArgs): string {
84
+ const id = readString(args.id);
85
+ if (!id) throw new Error('id is required.');
86
+ return id;
87
+ }
88
+
89
+ export function requireConfirmedDelete(args: AgentLocalRegistryToolArgs, label: string): void {
90
+ const explicitUserRequest = readString(args.explicitUserRequest);
91
+ if (!explicitUserRequest) throw new Error(`${label} deletion requires explicitUserRequest with the user's exact request or a short faithful summary.`);
92
+ if (args.confirm !== true) throw new Error(`${label} deletion requires confirm:true after an explicit user request.`);
93
+ }
94
+
95
+ export function requireName(args: AgentLocalRegistryToolArgs): string {
96
+ const name = readString(args.name);
97
+ if (!name) throw new Error('name is required.');
98
+ return name;
99
+ }
100
+
101
+ export function requireDescription(args: AgentLocalRegistryToolArgs): string {
102
+ const description = readString(args.description);
103
+ if (!description) throw new Error('description is required.');
104
+ return description;
105
+ }
106
+
107
+ export function requireTextField(value: unknown, fieldName: string): string {
108
+ const text = readString(value);
109
+ if (!text) throw new Error(`${fieldName} is required.`);
110
+ return text;
111
+ }
112
+
113
+ export function requireSummary(args: AgentLocalRegistryToolArgs): string {
114
+ const summary = readString(args.summary || args.description);
115
+ if (!summary) throw new Error('summary is required.');
116
+ return summary;
117
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Agent-local memory domain handler (W4-A1: memory honesty).
3
+ *
4
+ * Split out of agent-local-registry-tool.ts to stay under the 800-line
5
+ * architecture cap. Owns the `memory` domain of the agent_local_registry
6
+ * tool: list/search/get/create/update/review/stale/delete.
7
+ *
8
+ * The `search` action defaults to semantic recall (registry.searchSemantic)
9
+ * instead of the old literal LIKE-scan default — natural-language recall must
10
+ * not depend on the query text appearing as a literal substring in the
11
+ * stored summary/detail. Index health is checked before every semantic
12
+ * search so an unavailable index degrades honestly (stated reason, literal
13
+ * fallback) instead of silently returning zero matches that read as "nothing
14
+ * was ever stored."
15
+ */
16
+ import {
17
+ HASHED_MEMORY_EMBEDDING_PROVIDER,
18
+ MemoryRegistry,
19
+ type MemoryClass,
20
+ type MemoryRecord,
21
+ type MemoryScope,
22
+ type MemorySemanticSearchResult,
23
+ type MemoryVectorStats,
24
+ } from '@pellux/goodvibes-sdk/platform/state';
25
+ import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
26
+ import { formatAgentRecordReference, formatAgentRecordReviewState } from '../agent/record-labels.ts';
27
+ import {
28
+ AGENT_TOOL_PROVENANCE,
29
+ type AgentLocalRegistryToolArgs,
30
+ readOptionalConfidence,
31
+ readString,
32
+ readStringList,
33
+ requireConfirmedDelete,
34
+ requireId,
35
+ requireSummary,
36
+ } from './agent-local-registry-args.ts';
37
+ // `import type` is erased at compile time, so this does not create a runtime
38
+ // circular dependency even though agent-local-registry-tool.ts imports
39
+ // handleMemory (below) from this module.
40
+ import type { AgentLocalRegistryAction } from './agent-local-registry-tool.ts';
41
+
42
+ export const MEMORY_CLASSES: readonly MemoryClass[] = ['decision', 'constraint', 'incident', 'pattern', 'fact', 'risk', 'runbook', 'architecture', 'ownership'];
43
+ export const MEMORY_SCOPES: readonly MemoryScope[] = ['session', 'project', 'team'];
44
+
45
+ function isMemoryClass(value: unknown): value is MemoryClass {
46
+ return typeof value === 'string' && MEMORY_CLASSES.includes(value as MemoryClass);
47
+ }
48
+
49
+ function isMemoryScope(value: unknown): value is MemoryScope {
50
+ return typeof value === 'string' && MEMORY_SCOPES.includes(value as MemoryScope);
51
+ }
52
+
53
+ function requireMemoryClass(args: AgentLocalRegistryToolArgs): MemoryClass {
54
+ const cls = args.cls || 'fact';
55
+ if (!isMemoryClass(cls)) throw new Error(`Invalid memory class. Valid values ${MEMORY_CLASSES.join(', ')}.`);
56
+ return cls;
57
+ }
58
+
59
+ function readMemoryScope(args: AgentLocalRegistryToolArgs): MemoryScope {
60
+ const scope = args.scope || 'project';
61
+ if (!isMemoryScope(scope)) throw new Error(`Invalid memory scope. Valid values ${MEMORY_SCOPES.join(', ')}.`);
62
+ return scope;
63
+ }
64
+
65
+ export function formatMemory(record: MemoryRecord): string {
66
+ const tags = record.tags.length > 0 ? ` tags ${record.tags.join(', ')}` : '';
67
+ return `${record.id} ${record.scope}/${record.cls} ${formatAgentRecordReviewState(record.reviewState)} ${record.confidence}%${tags} ${record.summary}`;
68
+ }
69
+
70
+ function formatMemoryMatch(entry: MemorySemanticSearchResult): string {
71
+ const matchLabel = entry.similarity > 0 ? ` match ${Math.round(entry.similarity * 100)}% (score ${Math.round(entry.score)})` : '';
72
+ return `${formatMemory(entry.record)}${matchLabel}`;
73
+ }
74
+
75
+ /**
76
+ * A HARD unavailable reason: the semantic index cannot be consulted at all, so the
77
+ * search action must fall back to a literal LIKE-scan and say so — never a silent
78
+ * empty result that reads as "no memory matches" when the index was never asked.
79
+ */
80
+ function describeMemoryIndexUnavailable(stats: MemoryVectorStats): string | null {
81
+ if (!stats.enabled) return 'the semantic memory index is disabled for this store';
82
+ if (!stats.available) return `the semantic memory index is unavailable${stats.error ? `: ${stats.error}` : ''}`;
83
+ if (stats.indexedRecords === 0) return 'the semantic memory index has no indexed records yet';
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * A SOFT caveat: the index is up and consulted, but it is running on the built-in
89
+ * hashed-only fallback embedding provider (bag-of-words/token hashing) rather than a
90
+ * real embedding model. That still ranks real matches meaningfully better than a
91
+ * literal substring scan, but it is not a modeled semantic understanding — say so
92
+ * rather than presenting it as equivalent to a configured semantic provider.
93
+ */
94
+ function describeMemoryIndexCaveat(stats: MemoryVectorStats): string | null {
95
+ if (stats.embeddingProviderId === HASHED_MEMORY_EMBEDDING_PROVIDER.id) {
96
+ return 'running on the hashed-only fallback embedding provider (no dedicated semantic model configured) — ranking is approximate, not modeled semantic understanding';
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function renderMemorySearch(mode: string, query: string, lines: readonly string[]): string {
102
+ if (lines.length === 0) {
103
+ return `Agent-local memory search (${mode})\nNo Agent-local memory records matched "${query}".`;
104
+ }
105
+ return [`Agent-local memory search (${mode})`, `query ${query || '(all)'}`, ...lines].join('\n');
106
+ }
107
+
108
+ export async function handleMemory(registry: MemoryRegistry, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): Promise<string> {
109
+ if (action === 'list') {
110
+ const records = registry.getAll();
111
+ return records.length === 0
112
+ ? 'Agent-local memory\nNo Agent-local memory records.'
113
+ : ['Agent-local memory', ...records.map(formatMemory)].join('\n');
114
+ }
115
+ if (action === 'search') {
116
+ const query = readString(args.query);
117
+ const wantsLiteral = args.semantic === false;
118
+ if (wantsLiteral) {
119
+ const records = registry.search({ query, limit: 10 });
120
+ return renderMemorySearch('literal — explicitly requested', query, records.map(formatMemory));
121
+ }
122
+ // SEMANTIC BY DEFAULT: natural-language recall must not depend on the query text
123
+ // appearing as a literal substring in the stored summary/detail. Check index
124
+ // health FIRST so an unavailable index degrades honestly instead of silently
125
+ // returning zero matches that read as "nothing was ever stored."
126
+ const stats = registry.vectorStats();
127
+ const unavailable = describeMemoryIndexUnavailable(stats);
128
+ if (unavailable) {
129
+ const records = registry.search({ query, limit: 10 });
130
+ return renderMemorySearch(`literal fallback — ${unavailable}`, query, records.map(formatMemory));
131
+ }
132
+ const results = registry.searchSemantic({ query, limit: 10 });
133
+ const consultedSemanticIndex = query.length > 0 && results.some((entry) => entry.similarity > 0);
134
+ const caveat = describeMemoryIndexCaveat(stats);
135
+ const mode = !consultedSemanticIndex && query.length > 0
136
+ ? 'semantic — no vector match for this query, ranked by stored confidence instead'
137
+ : caveat
138
+ ? `semantic — ${caveat}`
139
+ : 'semantic';
140
+ return renderMemorySearch(mode, query, results.map(formatMemoryMatch));
141
+ }
142
+ if (action === 'get') {
143
+ const record = registry.get(requireId(args));
144
+ if (!record) return `Unknown Agent-local memory ${readString(args.id)}`;
145
+ return [
146
+ formatMemory(record),
147
+ `created ${new Date(record.createdAt).toISOString()}`,
148
+ `updated ${new Date(record.updatedAt).toISOString()}`,
149
+ `origin ${record.provenance.map((entry) => formatAgentRecordReference({ kind: String(entry.kind), ref: String(entry.ref) })).join(', ') || '(none)'}`,
150
+ `provenance: ${record.provenance.map((entry) => `${String(entry.kind)}:${String(entry.ref)}`).join(',') || '(none)'}`,
151
+ '',
152
+ record.detail || '(no detail)',
153
+ ].join('\n');
154
+ }
155
+ if (action === 'create') {
156
+ const summary = requireSummary(args);
157
+ const detail = readString(args.detail || args.body);
158
+ const tags = readStringList(args.tags);
159
+ const confidence = readOptionalConfidence(args.confidence);
160
+ assertNoSecretLikeMemoryText([summary, detail, ...tags]);
161
+ const record = await registry.add({
162
+ scope: readMemoryScope(args),
163
+ cls: requireMemoryClass(args),
164
+ summary,
165
+ detail,
166
+ tags: [...tags],
167
+ ...(confidence === undefined ? {} : { review: { state: 'fresh' as const, confidence } }),
168
+ provenance: [{ kind: 'event', ref: readString(args.provenance) || AGENT_TOOL_PROVENANCE }],
169
+ });
170
+ return [
171
+ 'Created Agent-local memory',
172
+ ` id ${record.id}`,
173
+ ` summary ${record.summary}`,
174
+ ].join('\n');
175
+ }
176
+ if (action === 'update') {
177
+ const summary = readString(args.summary || args.description);
178
+ const detail = readString(args.detail || args.body);
179
+ const tags = args.tags === undefined ? undefined : [...readStringList(args.tags)];
180
+ assertNoSecretLikeMemoryText([summary, detail, ...(tags ?? [])]);
181
+ const record = registry.update(requireId(args), {
182
+ summary: summary || undefined,
183
+ detail: detail || undefined,
184
+ tags,
185
+ scope: args.scope === undefined ? undefined : readMemoryScope(args),
186
+ });
187
+ if (!record) return `Unknown Agent-local memory ${readString(args.id)}`;
188
+ return [
189
+ 'Updated Agent-local memory',
190
+ ` id ${record.id}`,
191
+ ` summary ${record.summary}`,
192
+ ].join('\n');
193
+ }
194
+ if (action === 'review') {
195
+ const record = registry.review(requireId(args), {
196
+ state: 'reviewed',
197
+ confidence: readOptionalConfidence(args.confidence),
198
+ reviewedBy: 'agent',
199
+ });
200
+ if (!record) return `Unknown Agent-local memory ${readString(args.id)}`;
201
+ return [
202
+ 'Reviewed Agent-local memory',
203
+ ` id ${record.id}`,
204
+ ].join('\n');
205
+ }
206
+ if (action === 'stale') {
207
+ const record = registry.review(requireId(args), {
208
+ state: 'stale',
209
+ staleReason: readString(args.reason) || 'Marked stale by Agent.',
210
+ });
211
+ if (!record) return `Unknown Agent-local memory ${readString(args.id)}`;
212
+ return [
213
+ 'Marked Agent-local memory stale',
214
+ ` id ${record.id}`,
215
+ ].join('\n');
216
+ }
217
+ if (action === 'delete') {
218
+ const id = requireId(args);
219
+ requireConfirmedDelete(args, 'Agent-local memory');
220
+ if (!registry.delete(id)) return `Unknown Agent-local memory ${id}`;
221
+ return [
222
+ 'Deleted Agent-local memory',
223
+ ` id ${id}`,
224
+ ].join('\n');
225
+ }
226
+ throw new Error(`Action ${action} is not valid for memory.`);
227
+ }