pi-harness-runtime 1.1.183 → 1.1.185

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 (120) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/harness/glm-quota-countdown.ts +9 -1
  3. package/harness/honcho-init.ts +745 -0
  4. package/harness/honcho-memory.ts +290 -97
  5. package/harness/service-commands.ts +28 -3
  6. package/index.ts +191 -63
  7. package/package.json +1 -1
  8. package/packages/a2a-adapter/package.json +1 -1
  9. package/packages/agent-policy/dist/extension.d.ts.map +1 -1
  10. package/packages/agent-policy/dist/extension.js +16 -9
  11. package/packages/agent-policy/dist/extension.js.map +1 -1
  12. package/packages/agent-policy/dist/policy-loader.d.ts.map +1 -1
  13. package/packages/agent-policy/dist/policy-loader.js +5 -16
  14. package/packages/agent-policy/dist/policy-loader.js.map +1 -1
  15. package/packages/agent-policy/package.json +1 -1
  16. package/packages/agent-policy/src/extension.ts +16 -9
  17. package/packages/agent-policy/src/policy-loader.ts +5 -22
  18. package/packages/agent-policy/test/interlock.test.ts +101 -0
  19. package/packages/architecture-generator/package.json +1 -1
  20. package/packages/auth/package.json +1 -1
  21. package/packages/autonomous-refactor/package.json +1 -1
  22. package/packages/autonomous-runtime/package.json +1 -1
  23. package/packages/cache-strategy/package.json +1 -1
  24. package/packages/capability-registry/package.json +1 -1
  25. package/packages/checkpoint/package.json +1 -1
  26. package/packages/cli-plugin-sdk/package.json +1 -1
  27. package/packages/clipboard/package.json +1 -1
  28. package/packages/clipboard-plugin/package.json +1 -1
  29. package/packages/code-generation/package.json +1 -1
  30. package/packages/code-review/package.json +1 -1
  31. package/packages/codex-adapter/package.json +1 -1
  32. package/packages/codex-watcher/package.json +1 -1
  33. package/packages/config-capture/package.json +1 -1
  34. package/packages/context-compiler/package.json +1 -1
  35. package/packages/context-discovery/package.json +1 -1
  36. package/packages/context-manager/package.json +1 -1
  37. package/packages/cookie-sanitizer/package.json +1 -1
  38. package/packages/cost-optimizer/package.json +1 -1
  39. package/packages/dependency-analyzer/package.json +1 -1
  40. package/packages/django-plugin/package.json +1 -1
  41. package/packages/doc-generator/package.json +1 -1
  42. package/packages/e2e-tester/package.json +1 -1
  43. package/packages/evaluation-engine/package.json +1 -1
  44. package/packages/evaluation-runner/package.json +1 -1
  45. package/packages/event-bus/package.json +1 -1
  46. package/packages/event-store/package.json +1 -1
  47. package/packages/experience-replay/package.json +1 -1
  48. package/packages/feedback-collector/package.json +1 -1
  49. package/packages/file-copy-helper/package.json +1 -1
  50. package/packages/framework-detector/package.json +1 -1
  51. package/packages/framework-plugin-sdk/package.json +1 -1
  52. package/packages/frappe-plugin/package.json +1 -1
  53. package/packages/generic-web-plugin/package.json +1 -1
  54. package/packages/health-monitor/package.json +1 -1
  55. package/packages/intent-analyzer/package.json +1 -1
  56. package/packages/jev-judge/package.json +1 -1
  57. package/packages/knowledge-graph/package.json +1 -1
  58. package/packages/knowledge-retrieval/package.json +1 -1
  59. package/packages/laravel-plugin/package.json +1 -1
  60. package/packages/learning-engine/package.json +1 -1
  61. package/packages/mcp-adapter/package.json +1 -1
  62. package/packages/memory-engine/package.json +1 -1
  63. package/packages/milestone-manager/package.json +1 -1
  64. package/packages/model-registry/package.json +1 -1
  65. package/packages/nextjs-plugin/package.json +1 -1
  66. package/packages/notification/dist/adapters/telegram-adapter.d.ts +1 -1
  67. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
  68. package/packages/notification/dist/adapters/telegram-adapter.js +34 -21
  69. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  70. package/packages/notification/package.json +1 -1
  71. package/packages/notification/src/adapters/telegram-adapter.ts +37 -21
  72. package/packages/observability/package.json +1 -1
  73. package/packages/okf-indexer/package.json +1 -1
  74. package/packages/paperclip-integration/package.json +1 -1
  75. package/packages/performance-optimizer/package.json +1 -1
  76. package/packages/privilege-broker/package.json +1 -1
  77. package/packages/project-analyzer/package.json +1 -1
  78. package/packages/project-bootstrap/package.json +1 -1
  79. package/packages/projection-engine/package.json +1 -1
  80. package/packages/prompt-compiler/package.json +1 -1
  81. package/packages/prompt-versioning/package.json +1 -1
  82. package/packages/provider-adapter-sdk/package.json +1 -1
  83. package/packages/provider-router/package.json +1 -1
  84. package/packages/provider-selector/package.json +1 -1
  85. package/packages/providers/package.json +1 -1
  86. package/packages/qdrant-skills/package.json +1 -1
  87. package/packages/quota-manager/package.json +1 -1
  88. package/packages/rate-limiter/package.json +1 -1
  89. package/packages/react-vite-plugin/package.json +1 -1
  90. package/packages/release-manager/package.json +1 -1
  91. package/packages/requirement-compiler/package.json +1 -1
  92. package/packages/runtime/package.json +1 -1
  93. package/packages/scheduler/package.json +1 -1
  94. package/packages/scheduler-adapter/package.json +1 -1
  95. package/packages/session/package.json +1 -1
  96. package/packages/session-api/package.json +1 -1
  97. package/packages/session-export/package.json +1 -1
  98. package/packages/shared-context/package.json +1 -1
  99. package/packages/skill-mcp-client/package.json +1 -1
  100. package/packages/skill-registry/package.json +1 -1
  101. package/packages/skills/package.json +1 -1
  102. package/packages/sprint-planner/package.json +1 -1
  103. package/packages/subscription-engine/package.json +1 -1
  104. package/packages/task-compiler/package.json +1 -1
  105. package/packages/telegram/package.json +1 -1
  106. package/packages/tencentdb-memory/package.json +1 -1
  107. package/packages/tencentdb-sync/package.json +1 -1
  108. package/packages/test-data-generator/package.json +1 -1
  109. package/packages/test-generator/package.json +1 -1
  110. package/packages/todo-bd-sync/package.json +1 -1
  111. package/packages/token-estimation/package.json +1 -1
  112. package/packages/token-optimizer/package.json +1 -1
  113. package/packages/trajectory/package.json +1 -1
  114. package/packages/tui/package.json +1 -1
  115. package/packages/types/package.json +1 -1
  116. package/packages/webmcp-tester/package.json +1 -1
  117. package/packages/workflow-events/package.json +1 -1
  118. package/packages/workspace-scanner/package.json +1 -1
  119. package/packages/worktree/package.json +1 -1
  120. package/packages/write-review/package.json +1 -1
@@ -12,79 +12,151 @@
12
12
  */
13
13
 
14
14
  import { logServiceEvent, honchoStatus } from "./service-diagnostics.js";
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { honchoToolCall } from "./honcho-init.js";
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
16
17
  import { join } from "node:path";
17
18
  import { homedir } from "node:os";
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
- // Auto-create MCP config for Honcho Cloud
21
- // ----------------------------------------------------------------------------
21
+ // Honcho MCP Configuration (non-destructive)
22
+ // ---------------------------------------------------------------------------
22
23
 
23
24
  const MCP_CONFIG_DIR = join(homedir(), ".config", "mcp");
24
25
  const MCP_CONFIG_FILE = join(MCP_CONFIG_DIR, "mcp.json");
25
26
  const HONCHO_KEY_FILE = join(homedir(), ".pi-harness-runtime", "keys", "honcho-api-key.txt");
26
27
 
27
- function ensureMcpConfig(): void {
28
- // Create config directory if needed
28
+ /**
29
+ * Secrets to redact from messages before transmission.
30
+ * These patterns are checked case-insensitively.
31
+ * Matches: key=VALUE, key: VALUE, key: Bearer VALUE, etc.
32
+ * For compound keys like api_key, match the full key name.
33
+ */
34
+ const SECRET_PATTERNS: (string | RegExp)[] = [
35
+ // Compound patterns (check first, more specific)
36
+ "api[_-]?key",
37
+ "honcho[_-]?api[_-]?key",
38
+ "minimax[_-]?api[_-]?key",
39
+ "openai[_-]?api[_-]?key",
40
+ // Simple patterns
41
+ "auth",
42
+ "authorization",
43
+ "bearer",
44
+ "token",
45
+ "secret",
46
+ "password",
47
+ "credential",
48
+ ];
49
+
50
+ /**
51
+ * Redact secrets from text content.
52
+ * Replaces secret VALUES with [REDACTED].
53
+ * Handles: password=VALUE, password: VALUE, auth: Bearer VALUE, etc.
54
+ */
55
+ function redactSecrets(text: string): string {
56
+ let result = text;
57
+ for (const pattern of SECRET_PATTERNS) {
58
+ if (typeof pattern === "string") {
59
+ // Escape special regex chars in pattern string
60
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
+ // Match key=VALUE, key: VALUE, key: Bearer VALUE patterns
62
+ // Allow optional space after colon and multi-word values
63
+ // The separator can be =, :, or _ (for compound keys like api_key_value)
64
+ const regex = new RegExp(
65
+ `(${escaped})(?:[_=:]\\s*)(\\S+(?:\\s+\\S+)*)`,
66
+ "gi"
67
+ );
68
+ result = result.replace(regex, "$1=[REDACTED]");
69
+ } else {
70
+ // For regex patterns, redact the entire match
71
+ // Add 'g' flag if not present
72
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
73
+ const globalPattern = new RegExp(pattern.source, flags);
74
+ result = result.replace(globalPattern, "[REDACTED]");
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Ensure MCP config directory exists (non-destructive).
82
+ * Does NOT write config at import time.
83
+ */
84
+ export function ensureMcpConfigDirectory(): void {
29
85
  if (!existsSync(MCP_CONFIG_DIR)) {
30
86
  mkdirSync(MCP_CONFIG_DIR, { recursive: true, mode: 0o755 });
31
87
  }
88
+ }
32
89
 
33
- // Check if Honcho MCP is already configured with real key
34
- if (existsSync(MCP_CONFIG_FILE)) {
90
+ /**
91
+ * Load Honcho API key from keys directory.
92
+ * Does NOT write config files.
93
+ */
94
+ export function loadHonchoApiKey(): string | undefined {
95
+ if (existsSync(HONCHO_KEY_FILE)) {
35
96
  try {
36
- const content = readFileSync(MCP_CONFIG_FILE, "utf8");
37
- const config = JSON.parse(content);
38
- if (config.mcpServers?.honcho?.bearerToken &&
39
- config.mcpServers.honcho.bearerToken !== "YOUR_HONCHO_API_KEY") {
40
- return; // Already configured with real key
41
- }
97
+ return readFileSync(HONCHO_KEY_FILE, "utf8").trim() || undefined;
42
98
  } catch {
43
- // Invalid JSON, will overwrite
99
+ return undefined;
44
100
  }
45
101
  }
102
+ return undefined;
103
+ }
46
104
 
47
- // Try to read API key from keys directory
48
- let apiKey = "";
49
- if (existsSync(HONCHO_KEY_FILE)) {
50
- try {
51
- apiKey = readFileSync(HONCHO_KEY_FILE, "utf8").trim();
52
- } catch {
53
- // Will use placeholder
54
- }
105
+ /**
106
+ * Read existing MCP config (non-destructive).
107
+ * Returns null if config doesn't exist or is invalid.
108
+ */
109
+ export function readMcpConfig(): Record<string, unknown> | null {
110
+ if (!existsSync(MCP_CONFIG_FILE)) {
111
+ return null;
55
112
  }
113
+ try {
114
+ const content = readFileSync(MCP_CONFIG_FILE, "utf8");
115
+ return JSON.parse(content);
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
56
120
 
57
- // Create config with proper Honcho MCP headers
58
- const config = {
59
- mcpServers: {
60
- honcho: {
61
- url: "https://mcp.honcho.dev",
62
- headers: {
63
- Authorization: `Bearer ${apiKey || "YOUR_HONCHO_API_KEY"}`,
64
- },
65
- },
121
+ /**
122
+ * Write MCP config atomically (non-destructive).
123
+ * Only updates the Honcho server entry, preserves other entries.
124
+ */
125
+ export function writeHonchoMcpConfig(apiKey: string): void {
126
+ ensureMcpConfigDirectory();
127
+
128
+ // Read existing config or create new one
129
+ let config: Record<string, unknown> = readMcpConfig() || { mcpServers: {} };
130
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
131
+ config = { mcpServers: {} };
132
+ }
133
+
134
+ // Update Honcho entry
135
+ const mcpServers = config.mcpServers as Record<string, unknown>;
136
+ mcpServers.honcho = {
137
+ url: "https://mcp.honcho.dev",
138
+ headers: {
139
+ Authorization: `Bearer ${apiKey}`,
66
140
  },
67
141
  };
68
142
 
143
+ // Atomic write with temp file
144
+ const tempFile = `${MCP_CONFIG_FILE}.tmp`;
145
+ writeFileSync(tempFile, JSON.stringify(config, null, 2), { mode: 0o644 });
69
146
  try {
70
- writeFileSync(MCP_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o644 });
71
- if (apiKey) {
72
- console.log("[honcho] MCP config created with key from:", HONCHO_KEY_FILE);
73
- } else {
74
- console.log("[honcho] MCP config created at:", MCP_CONFIG_FILE);
75
- console.log("[honcho] NOTE: Run: echo \"YOUR_KEY\" > ~/.pi-harness-runtime/keys/honcho-api-key.txt");
76
- }
77
- } catch (err) {
78
- console.warn("[honcho] Failed to create MCP config:", err);
147
+ // Verify the temp file is valid JSON before renaming
148
+ JSON.parse(readFileSync(tempFile, "utf8"));
149
+ renameSync(tempFile, MCP_CONFIG_FILE);
150
+ } catch {
151
+ // Clean up invalid temp file
152
+ try { unlinkSync(tempFile); } catch { /* ignore */ }
153
+ throw new Error("Failed to write valid MCP config");
79
154
  }
80
155
  }
81
156
 
82
- // Auto-create MCP config on module load
83
- ensureMcpConfig();
84
-
85
157
  // ---------------------------------------------------------------------------
86
158
  // Honcho Health Check - Detect suspended accounts
87
- // ----------------------------------------------------------------------------
159
+ // ---------------------------------------------------------------------------
88
160
 
89
161
  interface HonchoHealthResponse {
90
162
  status: string;
@@ -96,13 +168,8 @@ let _honchoHealthInterval: ReturnType<typeof setInterval> | null = null;
96
168
  const HONCHO_HEALTH_CHECK_INTERVAL = 5 * 60 * 1000; // Check every 5 minutes
97
169
  const HONCHO_HEALTH_ENDPOINT = "https://api.honcho.dev/v1/health";
98
170
 
99
- // Start only after the timer state and health-check constants are initialized.
100
- startHonchoHealthCheck();
101
-
102
171
  async function checkHonchoHealth(): Promise<void> {
103
- const apiKey = existsSync(HONCHO_KEY_FILE)
104
- ? readFileSync(HONCHO_KEY_FILE, "utf8").trim()
105
- : null;
172
+ const apiKey = loadHonchoApiKey();
106
173
 
107
174
  if (!apiKey) {
108
175
  return; // No key, skip health check
@@ -166,6 +233,10 @@ export function stopHonchoHealthCheck(): void {
166
233
  }
167
234
  }
168
235
 
236
+ // ---------------------------------------------------------------------------
237
+ // Honcho Memory Implementation
238
+ // ---------------------------------------------------------------------------
239
+
169
240
  /**
170
241
  * Honcho memory configuration
171
242
  */
@@ -178,6 +249,8 @@ export interface HonchoMemoryConfig {
178
249
  userPeerId?: string;
179
250
  /** Assistant peer ID */
180
251
  assistantPeerId?: string;
252
+ /** Session ID for memory storage */
253
+ sessionId?: string;
181
254
  /** Max context size to inject (chars) */
182
255
  maxContextChars?: number;
183
256
  /** Enable auto-ingestion */
@@ -201,6 +274,8 @@ export class HonchoMemory {
201
274
  private isConnected = false;
202
275
  private toolsDiscovered = false;
203
276
  private workspaceId: string | null = null;
277
+ private sessionId: string | null = null;
278
+ private _disposed = false;
204
279
 
205
280
  constructor(config: HonchoMemoryConfig) {
206
281
  this.config = {
@@ -208,6 +283,8 @@ export class HonchoMemory {
208
283
  autoIngest: true,
209
284
  ...config,
210
285
  };
286
+ // Store session ID from config
287
+ this.sessionId = config.sessionId ?? null;
211
288
  }
212
289
 
213
290
  /**
@@ -258,6 +335,21 @@ export class HonchoMemory {
258
335
  });
259
336
  }
260
337
 
338
+ /**
339
+ * Set session ID for memory storage
340
+ */
341
+ setSession(sessionId: string): void {
342
+ this.sessionId = sessionId;
343
+ logServiceEvent({
344
+ ts: new Date().toISOString(),
345
+ level: "info",
346
+ service: "honcho",
347
+ operation: "session_created",
348
+ phase: "completed",
349
+ sessionId,
350
+ });
351
+ }
352
+
261
353
  /**
262
354
  * Check if memory is ready
263
355
  */
@@ -266,15 +358,18 @@ export class HonchoMemory {
266
358
  }
267
359
 
268
360
  /**
269
- * Generate deduplication ID for a message
361
+ * Generate stable deduplication ID for a message.
362
+ * Uses content hash + event identity (not current time) for retry stability.
270
363
  */
271
- private generateMessageId(
364
+ generateMessageId(
365
+ eventId: string,
272
366
  role: string,
273
367
  content: string,
274
368
  timestamp: Date
275
369
  ): string {
276
- // Simple hash-based ID for deduplication
277
- const data = `${role}:${content}:${timestamp.toISOString()}`;
370
+ // Stable hash: eventId ensures retry stability
371
+ // timestamp is from the original event, not Date.now()
372
+ const data = `${eventId}:${role}:${content}:${timestamp.toISOString()}`;
278
373
  let hash = 0;
279
374
  for (let i = 0; i < data.length; i++) {
280
375
  const char = data.charCodeAt(i);
@@ -285,39 +380,70 @@ export class HonchoMemory {
285
380
  }
286
381
 
287
382
  /**
288
- * Sanitize message content for storage
289
- * Removes sensitive data, tool calls, etc.
383
+ * Sanitize message content for storage.
384
+ * Redacts secrets and enforces total payload bound.
290
385
  */
291
- private sanitizeContent(
386
+ sanitizeContent(
292
387
  content: string | Array<{ type?: string; text?: string }>
293
- ): string {
294
- if (typeof content === "string") {
295
- // Truncate very long messages
296
- return content.slice(0, 2000);
297
- }
388
+ ): { text: string; originalLength: number } {
389
+ let text: string;
298
390
 
299
- // Handle array content (TextContent blocks)
300
- if (Array.isArray(content)) {
301
- return content
391
+ if (typeof content === "string") {
392
+ text = content;
393
+ } else if (Array.isArray(content)) {
394
+ text = content
302
395
  .filter((block) => block.type === "text")
303
- .map((block) => {
304
- const text = (block as { text?: string }).text ?? "";
305
- return text.slice(0, 2000);
306
- })
396
+ .map((block) => (block as { text?: string }).text ?? "")
307
397
  .join("\n");
398
+ } else {
399
+ text = String(content);
400
+ }
401
+
402
+ const originalLength = text.length;
403
+
404
+ // Step 1: Redact secrets before truncation
405
+ text = redactSecrets(text);
406
+
407
+ // Step 2: Truncate to per-block limit
408
+ const BLOCK_LIMIT = 2000;
409
+ if (text.length > BLOCK_LIMIT) {
410
+ text = text.slice(0, BLOCK_LIMIT) + "...[truncated]";
411
+ }
412
+
413
+ // Step 3: Enforce total payload bound (separate limit for ingestion vs retrieval)
414
+ const TOTAL_LIMIT = 10000;
415
+ if (text.length > TOTAL_LIMIT) {
416
+ text = text.slice(0, TOTAL_LIMIT) + "...[payload truncated]";
308
417
  }
309
418
 
310
- return String(content).slice(0, 2000);
419
+ return { text, originalLength };
311
420
  }
312
421
 
313
422
  /**
314
- * Ingest a completed turn message
423
+ * Call a Honcho MCP tool through the adapter.
424
+ * Uses the honchoToolCall from honcho-init.ts.
425
+ */
426
+ private async callHonchoTool(
427
+ toolName: string,
428
+ args: Record<string, unknown>
429
+ ): Promise<{ success: boolean; result?: unknown; error?: string }> {
430
+ return await honchoToolCall(toolName, args);
431
+ }
432
+
433
+ /**
434
+ * Ingest a completed turn message through Honcho MCP.
435
+ * Persists only after verified successful response.
315
436
  */
316
437
  async ingestMessage(
317
438
  role: "user" | "assistant",
318
439
  content: string | Array<{ type?: string; text?: string }>,
319
- timestamp: Date = new Date()
440
+ timestamp: Date = new Date(),
441
+ eventId: string = `msg-${timestamp.getTime()}`
320
442
  ): Promise<{ success: boolean; deduped: boolean; error?: string }> {
443
+ if (this._disposed) {
444
+ return { success: false, deduped: false, error: "memory disposed" };
445
+ }
446
+
321
447
  if (!this.isReady()) {
322
448
  return { success: false, deduped: false, error: "memory not ready" };
323
449
  }
@@ -326,9 +452,10 @@ export class HonchoMemory {
326
452
  return { success: false, deduped: false, error: "auto-ingest disabled" };
327
453
  }
328
454
 
329
- const messageId = this.generateMessageId(role, String(content), timestamp);
455
+ const { text: sanitized, originalLength } = this.sanitizeContent(content);
456
+ const messageId = this.generateMessageId(eventId, role, sanitized, timestamp);
330
457
 
331
- // Check deduplication
458
+ // Check deduplication BEFORE any remote call
332
459
  if (this.ingestedMessages.has(messageId)) {
333
460
  logServiceEvent({
334
461
  ts: new Date().toISOString(),
@@ -342,22 +469,34 @@ export class HonchoMemory {
342
469
  return { success: true, deduped: true };
343
470
  }
344
471
 
345
- const sanitized = this.sanitizeContent(content);
472
+ logServiceEvent({
473
+ ts: new Date().toISOString(),
474
+ level: "info",
475
+ service: "honcho",
476
+ operation: "ingest_started",
477
+ phase: "start",
478
+ role,
479
+ messageLength: sanitized.length,
480
+ originalLength,
481
+ });
346
482
 
347
483
  try {
348
- // Call Honcho add_messages_to_session tool
349
- // Note: This assumes the MCP tool is available through the adapter
350
- logServiceEvent({
351
- ts: new Date().toISOString(),
352
- level: "info",
353
- service: "honcho",
354
- operation: "ingest_started",
355
- phase: "start",
484
+ // Call Honcho MCP tool through the adapter
485
+ // Use sessionId if available, fall back to workspaceId
486
+ const targetSessionId = this.sessionId ?? this.workspaceId;
487
+ const result = await this.callHonchoTool("add_message", {
488
+ session_id: targetSessionId,
356
489
  role,
357
- messageLength: sanitized.length,
490
+ content: sanitized,
491
+ message_id: messageId,
492
+ timestamp: timestamp.toISOString(),
358
493
  });
359
494
 
360
- // Store message for deduplication
495
+ if (!result.success) {
496
+ throw new Error(result.error || "Tool call failed");
497
+ }
498
+
499
+ // Record success ONLY after verified response
361
500
  this.ingestedMessages.set(messageId, {
362
501
  id: messageId,
363
502
  ingestedAt: timestamp,
@@ -403,7 +542,8 @@ export class HonchoMemory {
403
542
  }
404
543
 
405
544
  /**
406
- * Retrieve relevant context before agent execution
545
+ * Retrieve relevant context from Honcho MCP.
546
+ * Returns real retrieved content, never fabricates.
407
547
  */
408
548
  async retrieveContext(query: string): Promise<{
409
549
  success: boolean;
@@ -411,24 +551,55 @@ export class HonchoMemory {
411
551
  empty: boolean;
412
552
  error?: string;
413
553
  }> {
554
+ if (this._disposed) {
555
+ return { success: false, context: "", empty: true, error: "memory disposed" };
556
+ }
557
+
414
558
  if (!this.isReady()) {
415
559
  return { success: false, context: "", empty: true, error: "memory not ready" };
416
560
  }
417
561
 
562
+ logServiceEvent({
563
+ ts: new Date().toISOString(),
564
+ level: "info",
565
+ service: "honcho",
566
+ operation: "retrieve_started",
567
+ phase: "start",
568
+ queryLength: query.length,
569
+ });
570
+
418
571
  try {
419
- logServiceEvent({
420
- ts: new Date().toISOString(),
421
- level: "info",
422
- service: "honcho",
423
- operation: "retrieve_started",
424
- phase: "start",
425
- queryLength: query.length,
572
+ // Call Honcho get_session_context tool through the adapter
573
+ // Use sessionId if available, fall back to workspaceId
574
+ const targetSessionId = this.sessionId ?? this.workspaceId;
575
+ const result = await this.callHonchoTool("get_context", {
576
+ session_id: targetSessionId,
577
+ query,
578
+ limit: 10,
426
579
  });
427
580
 
428
- // Call Honcho get_session_context tool
429
- // Note: This assumes the MCP tool is available through the adapter
581
+ if (!result.success) {
582
+ throw new Error(result.error || "Tool call failed");
583
+ }
584
+
585
+ // Parse actual response content
586
+ let context = "";
587
+ if (result.result && typeof result.result === "object") {
588
+ const response = result.result as Record<string, unknown>;
589
+ // Extract context from response based on actual Honcho API shape
590
+ context = (response.content as string) ||
591
+ (response.messages as string[])?.join("\n") ||
592
+ JSON.stringify(response);
593
+ }
594
+
595
+ // Apply max context chars limit
596
+ const maxChars = this.config.maxContextChars ?? 4000;
597
+ if (context.length > maxChars) {
598
+ context = context.slice(0, maxChars) + "...[context truncated]";
599
+ }
430
600
 
431
- const context = `Retrieved context for: ${query.slice(0, 100)}...`;
601
+ // Empty retrieval is a valid result, not a fabricated context
602
+ const isEmpty = context.trim().length === 0;
432
603
 
433
604
  logServiceEvent({
434
605
  ts: new Date().toISOString(),
@@ -437,6 +608,7 @@ export class HonchoMemory {
437
608
  operation: "retrieve_succeeded",
438
609
  phase: "completed",
439
610
  contextLength: context.length,
611
+ empty: isEmpty,
440
612
  });
441
613
 
442
614
  honchoStatus.recordSuccess("retrieve", undefined, { contextLength: context.length });
@@ -444,7 +616,7 @@ export class HonchoMemory {
444
616
  return {
445
617
  success: true,
446
618
  context,
447
- empty: context.length === 0,
619
+ empty: isEmpty,
448
620
  };
449
621
  } catch (err) {
450
622
  const error = err instanceof Error ? err.message : String(err);
@@ -459,6 +631,7 @@ export class HonchoMemory {
459
631
 
460
632
  honchoStatus.recordFailure("retrieve", "unknown", { error });
461
633
 
634
+ // On failure, return empty context (don't fabricate)
462
635
  return { success: false, context: "", empty: true, error };
463
636
  }
464
637
  }
@@ -481,4 +654,24 @@ export class HonchoMemory {
481
654
  ingestedCount: this.ingestedMessages.size,
482
655
  };
483
656
  }
657
+
658
+ /**
659
+ * Dispose of the memory instance.
660
+ * Stops timers, clears state, prevents further operations.
661
+ */
662
+ dispose(): void {
663
+ this._disposed = true;
664
+ this.ingestedMessages.clear();
665
+ honchoStatus.setConnected(false);
666
+ logServiceEvent({
667
+ ts: new Date().toISOString(),
668
+ level: "info",
669
+ service: "honcho",
670
+ operation: "disposed",
671
+ phase: "completed",
672
+ });
673
+ }
484
674
  }
675
+
676
+ // Export redactSecrets for testing
677
+ export { redactSecrets };
@@ -116,6 +116,8 @@ async function handleTestJev(ctx: ExtensionContext, flags: Record<string, string
116
116
 
117
117
  /**
118
118
  * Handle /harness-services test honcho --read-only
119
+ * Checks Honcho integration status through the MCP adapter.
120
+ * Does NOT create sessions/peers, write messages, or claim success when only flags were inspected.
119
121
  */
120
122
  async function handleTestHoncho(ctx: ExtensionContext, flags: Record<string, string | boolean>): Promise<void> {
121
123
  if (flags.read_only !== true && flags.read_only !== "true") {
@@ -126,9 +128,32 @@ async function handleTestHoncho(ctx: ExtensionContext, flags: Record<string, str
126
128
  ctx.ui.notify("[services] Testing Honcho MCP connection...", "info");
127
129
 
128
130
  try {
129
- ctx.ui.notify("[services] Honcho status: registered=true, connected=" + honchoStatus.connected, "info");
130
- ctx.ui.notify("[services] Honcho tools discovery: would call tools/list if --read-only not set", "info");
131
- ctx.ui.notify("[services] Honcho test completed (read-only mode)", "info");
131
+ // Report actual registration status from the status tracker
132
+ ctx.ui.notify(`[services] Honcho configured: ${honchoStatus.configured}`, "info");
133
+ ctx.ui.notify(`[services] Honcho connected: ${honchoStatus.connected}`, "info");
134
+ ctx.ui.notify(`[services] Honcho feature enabled: ${honchoStatus.featureEnabled}`, "info");
135
+
136
+ // Check if MCP adapter is available via globalThis
137
+ const piEvents = (globalThis as { pi?: { events?: { emit: (event: string, req: unknown) } } }).pi?.events;
138
+
139
+ if (!piEvents) {
140
+ ctx.ui.notify("[services] Honcho test skipped: MCP adapter not available (pi.events)", "warning");
141
+ ctx.ui.notify("[services] Note: Install pi-mcp-adapter to enable Honcho MCP", "info");
142
+ return;
143
+ }
144
+
145
+ // For read-only test, report current status from our tracker
146
+ // A full MCP tool discovery would require a non-read-only request
147
+ if (honchoStatus.connected) {
148
+ ctx.ui.notify("[services] Honcho MCP server is connected", "info");
149
+ ctx.ui.notify("[services] Honcho test completed successfully", "info");
150
+ } else if (honchoStatus.configured) {
151
+ ctx.ui.notify("[services] Honcho MCP server not yet connected", "warning");
152
+ ctx.ui.notify("[services] Connection will be established on first use", "info");
153
+ } else {
154
+ ctx.ui.notify("[services] Honcho MCP not configured", "warning");
155
+ ctx.ui.notify("[services] Add API key to enable Honcho memory", "info");
156
+ }
132
157
  } catch (err) {
133
158
  const error = err instanceof Error ? err.message : String(err);
134
159
  ctx.ui.notify(`[services] Honcho test failed: ${error}`, "error");