supipowers 2.0.1 → 2.1.0

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 (80) hide show
  1. package/README.md +10 -6
  2. package/package.json +4 -2
  3. package/skills/harness/SKILL.md +1 -0
  4. package/src/bootstrap.ts +5 -133
  5. package/src/commands/clear.ts +6 -6
  6. package/src/commands/release.ts +3 -1
  7. package/src/commands/update.ts +1 -1
  8. package/src/config/defaults.ts +5 -5
  9. package/src/config/loader.ts +1 -0
  10. package/src/config/schema.ts +2 -6
  11. package/src/context/analyzer.ts +104 -35
  12. package/src/context-mode/knowledge/store.ts +381 -43
  13. package/src/context-mode/tools.ts +41 -3
  14. package/src/deps/registry.ts +1 -12
  15. package/src/fix-pr/assessment.ts +1 -0
  16. package/src/fix-pr/prompt-builder.ts +1 -0
  17. package/src/git/commit.ts +76 -18
  18. package/src/harness/command.ts +103 -6
  19. package/src/harness/default-agents/docs.md +39 -0
  20. package/src/harness/docs/config.ts +29 -0
  21. package/src/harness/docs/glob-match.ts +27 -0
  22. package/src/harness/docs/index-renderer.ts +82 -0
  23. package/src/harness/docs/provenance.ts +125 -0
  24. package/src/harness/docs/regen-decision.ts +167 -0
  25. package/src/harness/docs/representative-files.ts +175 -0
  26. package/src/harness/docs/source-hash.ts +106 -0
  27. package/src/harness/docs/validator.ts +233 -0
  28. package/src/harness/hooks/layer-context-inject.ts +35 -1
  29. package/src/harness/hooks/register.ts +24 -3
  30. package/src/harness/pipeline.ts +20 -5
  31. package/src/harness/pr-comment/baseline.ts +105 -0
  32. package/src/harness/pr-comment/ci-env.ts +120 -0
  33. package/src/harness/pr-comment/gh-poster.ts +227 -0
  34. package/src/harness/pr-comment/handler.ts +198 -0
  35. package/src/harness/pr-comment/render.ts +297 -0
  36. package/src/harness/pr-comment/status.ts +95 -0
  37. package/src/harness/pr-comment/types.ts +73 -0
  38. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  39. package/src/harness/project-paths.ts +95 -0
  40. package/src/harness/stages/design.ts +1 -0
  41. package/src/harness/stages/discover.ts +1 -13
  42. package/src/harness/stages/docs.ts +708 -0
  43. package/src/harness/stages/implement-apply.ts +877 -0
  44. package/src/harness/stages/implement.ts +64 -51
  45. package/src/harness/stages/plan.ts +25 -16
  46. package/src/harness/stages/validate.ts +370 -0
  47. package/src/harness/storage.ts +142 -0
  48. package/src/harness/tools.ts +130 -0
  49. package/src/mempalace/bridge.ts +207 -41
  50. package/src/mempalace/config.ts +10 -4
  51. package/src/mempalace/format.ts +122 -6
  52. package/src/mempalace/hooks.ts +204 -56
  53. package/src/mempalace/installer-helper.ts +18 -4
  54. package/src/mempalace/python/mempalace_bridge.py +128 -3
  55. package/src/mempalace/runtime.ts +55 -18
  56. package/src/mempalace/schema.ts +151 -30
  57. package/src/mempalace/session-summary.ts +5 -0
  58. package/src/mempalace/tool.ts +17 -4
  59. package/src/mempalace/upstream-limits.ts +69 -0
  60. package/src/planning/approval-flow.ts +25 -2
  61. package/src/planning/planning-ask-tool.ts +34 -4
  62. package/src/planning/system-prompt.ts +1 -1
  63. package/src/tool-catalog/active-tool-controller.ts +0 -22
  64. package/src/tool-catalog/active-tool-planner.ts +0 -26
  65. package/src/tool-catalog/tool-groups.ts +1 -9
  66. package/src/types.ts +87 -8
  67. package/src/ui-design/session.ts +114 -10
  68. package/src/utils/executable.ts +10 -1
  69. package/src/workspace/state-paths.ts +1 -1
  70. package/src/commands/mcp.ts +0 -814
  71. package/src/mcp/activation.ts +0 -77
  72. package/src/mcp/config.ts +0 -223
  73. package/src/mcp/docs.ts +0 -154
  74. package/src/mcp/gateway.ts +0 -103
  75. package/src/mcp/lifecycle.ts +0 -79
  76. package/src/mcp/manager-tool.ts +0 -104
  77. package/src/mcp/mcpc.ts +0 -113
  78. package/src/mcp/registry.ts +0 -98
  79. package/src/mcp/triggers.ts +0 -62
  80. package/src/mcp/types.ts +0 -95
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """One-shot JSON bridge for the native supipowers MemPalace tool.
3
3
 
4
- Action mapping (auditable API surface) — wired against MemPalace 3.3.4:
4
+ Action mapping (auditable API surface):
5
5
  - version: stdlib importlib.metadata only; does not import MemPalace runtime modules.
6
6
  - 29 MCP-equivalent actions dispatch to ``mempalace.mcp_server.tool_<action>``
7
7
  (with the ``traverse → tool_traverse_graph`` rename). The mcp_server
@@ -129,6 +129,8 @@ def _handle_version(params: Dict[str, Any], options: Dict[str, Any]) -> Dict[str
129
129
 
130
130
  def _apply_palace_path(options: Dict[str, Any]) -> None:
131
131
  """Set MEMPALACE_PALACE_PATH so MemPalace's MempalaceConfig picks it up."""
132
+ # NOTE: mutates os.environ — safe only because we run in a fresh process per
133
+ # call. Any future daemon/persistent-process refactor MUST re-evaluate this.
132
134
  palace = options.get("palacePath") or options.get("palace")
133
135
  if palace and isinstance(palace, str) and palace:
134
136
  os.environ["MEMPALACE_PALACE_PATH"] = os.path.expanduser(palace)
@@ -203,6 +205,42 @@ def _rename(mapping: Dict[str, str]) -> Callable[[Dict[str, Any]], Dict[str, Any
203
205
  return extract
204
206
 
205
207
 
208
+
209
+ # ── diary_write: source_file embedding ───────────────────────────────────
210
+ #
211
+ # tool_diary_write(agent_name, entry, topic, wing) does not accept source_file.
212
+ # When a source_file is provided, embed it as a deterministic first-line prefix
213
+ # "[source: <source_file>]" in the entry text so that diary_read content
214
+ # carries the linkage. This is the Option-2 convention documented in
215
+ # session-summary.ts: the caller supplies source_file in params; the bridge
216
+ # embeds it; the round-trip proves it via content inspection.
217
+ DIARY_SOURCE_PREFIX = "[source: "
218
+
219
+ # Upstream sanitize_content default in mempalace.config (3.3.5). Mirrored in
220
+ # src/mempalace/upstream-limits.ts (MEMPALACE_MAX_CONTENT_LENGTH). Bump both
221
+ # when upgrading.
222
+ _MAX_CONTENT_LENGTH = 100_000
223
+
224
+
225
+ def _diary_write_extractor(params: Dict[str, Any]) -> Dict[str, Any]:
226
+ kwargs = _select("agent_name", "topic", "wing")(params)
227
+ entry: str = params.get("entry") or ""
228
+ source_file = params.get("source_file")
229
+ if source_file and isinstance(source_file, str):
230
+ # MemPalace's tool_diary_write calls sanitize_content(entry), which
231
+ # raises ValueError if len(entry) > MAX_CONTENT_LENGTH. Adding the
232
+ # deterministic `[source: ...]\n` prefix can push a TS-valid entry
233
+ # past that limit and turn a previously-valid request into a domain
234
+ # failure. Reserve the prefix budget here by clipping the entry tail
235
+ # so the prefixed payload always fits.
236
+ prefix = f"{DIARY_SOURCE_PREFIX}{source_file}]\n"
237
+ budget = max(0, _MAX_CONTENT_LENGTH - len(prefix))
238
+ if len(entry) > budget:
239
+ entry = entry[:budget]
240
+ entry = f"{prefix}{entry}"
241
+ kwargs["entry"] = entry
242
+ return kwargs
243
+
206
244
  # ── MCP-equivalent action dispatch ───────────────────────────────────────
207
245
  #
208
246
  # Each entry maps our action -> (function name in mempalace.mcp_server,
@@ -224,7 +262,7 @@ MCP_TOOL_DISPATCH: Dict[str, "tuple[str, Callable[[Dict[str, Any]], Dict[str, An
224
262
  "delete_drawer": ("tool_delete_drawer", _select("drawer_id")),
225
263
  # Knowledge graph: MemPalace uses `entity` for the subject in queries.
226
264
  "kg_query": ("tool_kg_query", _rename({"subject": "entity", "as_of": "as_of", "direction": "direction"})),
227
- "kg_add": ("tool_kg_add", _select("subject", "predicate", "object", "valid_from", "source_closet")),
265
+ "kg_add": ("tool_kg_add", _select("subject", "predicate", "object", "valid_from", "valid_to", "source_file", "source_drawer_id")),
228
266
  "kg_invalidate": ("tool_kg_invalidate", _select("subject", "predicate", "object", "ended")),
229
267
  "kg_timeline": ("tool_kg_timeline", _rename({"subject": "entity"})),
230
268
  "kg_stats": ("tool_kg_stats", lambda p: {}),
@@ -239,7 +277,9 @@ MCP_TOOL_DISPATCH: Dict[str, "tuple[str, Callable[[Dict[str, Any]], Dict[str, An
239
277
  "delete_tunnel": ("tool_delete_tunnel", _select("tunnel_id")),
240
278
  # follow_tunnels: MemPalace uses wing/room (not source_wing/source_room).
241
279
  "follow_tunnels": ("tool_follow_tunnels", _rename({"source_wing": "wing", "source_room": "room"})),
242
- "diary_write": ("tool_diary_write", _select("agent_name", "entry", "topic", "wing")),
280
+ # diary_write: source_file is embedded as a prefix in the entry text by
281
+ # _diary_write_extractor — tool_diary_write does not accept source_file natively.
282
+ "diary_write": ("tool_diary_write", _diary_write_extractor),
243
283
  "diary_read": ("tool_diary_read", _select("agent_name", "wing")),
244
284
  "hook_settings": ("tool_hook_settings", lambda p: {}),
245
285
  "memories_filed_away": ("tool_memories_filed_away", lambda p: {}),
@@ -287,6 +327,84 @@ def _handle_wake_up(params: Dict[str, Any], options: Dict[str, Any]) -> Dict[str
287
327
  return _ok({"text": text}, options)
288
328
 
289
329
 
330
+
331
+ # ── wake_up_and_search: composite action (saves one python process spawn) ─
332
+
333
+
334
+ def _handle_wake_up_and_search(params: Dict[str, Any], options: Dict[str, Any]) -> Dict[str, Any]:
335
+ """Batch wake_up + optional search into a single python process.
336
+
337
+ Returns {"wake": {"text": ...}, "search": <tool_search payload or null>}.
338
+ Either half may fail independently; the other half is still returned.
339
+ """
340
+ _apply_palace_path(options)
341
+ palace = options.get("palacePath")
342
+
343
+ # ── Wake half ─────────────────────────────────────────────────────────────
344
+ wake_result: Any = None
345
+ wake_error: "str | None" = None
346
+ try:
347
+ layers = _import_or_raise("mempalace.layers")
348
+ stack = layers.MemoryStack(palace_path=palace) if palace else layers.MemoryStack()
349
+ wake_kwargs: Dict[str, Any] = {}
350
+ if params.get("wing"):
351
+ wake_kwargs["wing"] = params["wing"]
352
+ text = _wrap_runtime_errors("MemoryStack.wake_up", lambda: stack.wake_up(**wake_kwargs))
353
+ wake_result = {"text": text}
354
+ except BridgeDomainError as exc:
355
+ wake_error = f"{exc.code}: {exc.message}"
356
+ except Exception as exc: # pragma: no cover — defensive isolation
357
+ wake_error = str(exc)
358
+
359
+ # ── Search half (skipped when query is absent or empty) ───────────────────
360
+ search_result: Any = None
361
+ search_error: "str | None" = None
362
+ query = params.get("query")
363
+ if query and isinstance(query, str) and query.strip():
364
+ try:
365
+ module = _import_or_raise("mempalace.mcp_server")
366
+ tool_search = getattr(module, "tool_search", None)
367
+ if not callable(tool_search):
368
+ raise BridgeDomainError(
369
+ "mempalace_missing",
370
+ "mempalace.mcp_server.tool_search is not callable.",
371
+ "Upgrade the managed MemPalace runtime via `/supi:memory setup`.",
372
+ )
373
+ search_kwargs: Dict[str, Any] = {"query": query}
374
+ if params.get("wing"):
375
+ search_kwargs["wing"] = params["wing"]
376
+ if params.get("room"):
377
+ search_kwargs["room"] = params["room"]
378
+ if isinstance(params.get("limit"), int):
379
+ search_kwargs["limit"] = params["limit"]
380
+ raw = _wrap_runtime_errors(
381
+ "mempalace.mcp_server.tool_search",
382
+ lambda: tool_search(**search_kwargs),
383
+ )
384
+ normalized = _to_jsonable(raw)
385
+ if isinstance(normalized, dict) and normalized.get("ok") is False and isinstance(normalized.get("error"), dict):
386
+ err = normalized["error"]
387
+ code = err.get("code") if isinstance(err.get("code"), str) else "mempalace_runtime_error"
388
+ message = err.get("message") if isinstance(err.get("message"), str) else json.dumps(err, sort_keys=True)
389
+ search_error = f"{code}: {message}"
390
+ else:
391
+ search_result = normalized
392
+ except BridgeDomainError as exc:
393
+ # Surface as partial error so the caller can distinguish a real
394
+ # search failure from "no query / no hits". Mirrors the wake half.
395
+ search_error = f"{exc.code}: {exc.message}"
396
+ except Exception as exc: # pragma: no cover — defensive isolation
397
+ # Same contract: never let one half kill the other, but the caller
398
+ # gets a string they can render or log.
399
+ search_error = str(exc)
400
+
401
+ payload: Dict[str, Any] = {"wake": wake_result, "search": search_result}
402
+ if wake_error is not None:
403
+ payload["wake_error"] = wake_error
404
+ if search_error is not None:
405
+ payload["search_error"] = search_error
406
+ return _ok(payload, options)
407
+
290
408
  # ── Native CLI args builders ──────────────────────────────────────────────
291
409
 
292
410
 
@@ -307,6 +425,8 @@ def _make_cli_args_mine(params: Dict[str, Any]) -> "list[str]":
307
425
  args.append("--include-ignored")
308
426
  if params.get("no_gitignore"):
309
427
  args.append("--no-gitignore")
428
+ if params.get("dry_run"):
429
+ args.append("--dry-run")
310
430
  if params.get("extract"):
311
431
  args.append("--extract")
312
432
  return args
@@ -331,6 +451,10 @@ def _make_cli_args_repair(params: Dict[str, Any]) -> "list[str]":
331
451
  args.append("--yes")
332
452
  if params.get("mode"):
333
453
  args.extend(["--mode", str(params["mode"])])
454
+ if params.get("source"):
455
+ args.extend(["--source", str(params["source"])])
456
+ if params.get("archive_existing"):
457
+ args.append("--archive-existing")
334
458
  if params.get("dry_run"):
335
459
  args.append("--dry-run")
336
460
  return args
@@ -396,6 +520,7 @@ def _make_cli_handler(action: str) -> Callable[[Dict[str, Any], Dict[str, Any]],
396
520
  DISPATCH: Dict[str, Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]]] = {
397
521
  "version": _handle_version,
398
522
  "wake_up": _handle_wake_up,
523
+ "wake_up_and_search": _handle_wake_up_and_search,
399
524
  }
400
525
  for _action in MCP_TOOL_DISPATCH:
401
526
  DISPATCH[_action] = _make_mcp_handler(_action)
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import type { ResolvedMempalaceConfig } from "./config.js";
6
6
  import type { MempalaceAction, MempalaceParams } from "./schema.js";
7
+ import { ensureUv, type UvFetcher } from "./uv.js";
7
8
 
8
9
  export interface MempalaceRuntimeError {
9
10
  code: string;
@@ -18,6 +19,15 @@ export type BridgePathResult =
18
19
 
19
20
  export interface BridgePathOptions {
20
21
  moduleUrl?: string;
22
+ /**
23
+ * Installed extension package roots. Used when OMP executes temp-copied TS
24
+ * modules that do not preserve adjacent non-TypeScript assets.
25
+ */
26
+ extensionRoots?: string[];
27
+ }
28
+
29
+ export interface BridgePathPlatformPaths {
30
+ agent(...segments: string[]): string;
21
31
  }
22
32
 
23
33
  export interface ProcessRunResult {
@@ -73,6 +83,7 @@ export type RunBridgeRequestResult =
73
83
  stdoutPreview: string;
74
84
  stderrTail: string;
75
85
  durationMs: number;
86
+ completion?: Promise<void>;
76
87
  };
77
88
 
78
89
  export interface RunBridgeRequestOptions {
@@ -111,7 +122,7 @@ export interface SetupMempalaceRuntimeOptions {
111
122
  managedBinDir: string;
112
123
  managedPythonVersion?: string;
113
124
  runner?: ProcessRunner;
114
- fetcher?: import("./uv.js").UvFetcher;
125
+ fetcher?: UvFetcher;
115
126
  uvVersion?: string;
116
127
  onProgress?: (message: string) => void;
117
128
  /**
@@ -131,21 +142,39 @@ export interface SetupMempalaceRuntimeOptions {
131
142
  export function resolveBridgeScriptPath(options: BridgePathOptions = {}): BridgePathResult {
132
143
  const moduleUrl = options.moduleUrl ?? import.meta.url;
133
144
  const runtimePath = fileURLToPath(moduleUrl);
134
- const bridgePath = path.join(path.dirname(runtimePath), "python", "mempalace_bridge.py");
145
+ const moduleBridgePath = path.join(path.dirname(runtimePath), "python", "mempalace_bridge.py");
146
+ if (fs.existsSync(moduleBridgePath)) {
147
+ return { ok: true, path: moduleBridgePath };
148
+ }
135
149
 
136
- if (!fs.existsSync(bridgePath)) {
137
- return {
138
- ok: false,
139
- path: bridgePath,
140
- error: {
141
- code: "bridge_not_found",
142
- message: `Bundled MemPalace bridge not found at ${bridgePath}`,
143
- remediation: "Reinstall supipowers or verify the package includes src/mempalace/python/mempalace_bridge.py.",
144
- },
145
- };
150
+ let missingBridgePath = moduleBridgePath;
151
+ for (const extensionRoot of options.extensionRoots ?? []) {
152
+ const extensionBridgePath = path.join(extensionRoot, "src", "mempalace", "python", "mempalace_bridge.py");
153
+ missingBridgePath = extensionBridgePath;
154
+ if (fs.existsSync(extensionBridgePath)) {
155
+ return { ok: true, path: extensionBridgePath };
156
+ }
146
157
  }
147
158
 
148
- return { ok: true, path: bridgePath };
159
+ return {
160
+ ok: false,
161
+ path: missingBridgePath,
162
+ error: {
163
+ code: "bridge_not_found",
164
+ message: `Bundled MemPalace bridge not found at ${missingBridgePath}`,
165
+ remediation: "Reinstall supipowers or verify the package includes src/mempalace/python/mempalace_bridge.py.",
166
+ },
167
+ };
168
+ }
169
+
170
+ export function resolveInstalledBridgeScriptPath(
171
+ paths: BridgePathPlatformPaths,
172
+ options: BridgePathOptions = {},
173
+ ): BridgePathResult {
174
+ return resolveBridgeScriptPath({
175
+ ...options,
176
+ extensionRoots: [paths.agent("extensions", "supipowers"), ...(options.extensionRoots ?? [])],
177
+ });
149
178
  }
150
179
 
151
180
  export function resolveManagedVenvPaths(
@@ -295,8 +324,14 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
295
324
  const input = JSON.stringify(options.request);
296
325
  const runner = options.runner ?? defaultProcessRunner;
297
326
 
327
+ const runnerPromise = Promise.resolve().then(() =>
328
+ runner(options.pythonPath, [options.bridgeScriptPath], { input, timeoutMs: options.timeoutMs }),
329
+ );
330
+ const completion = runnerPromise.then(() => {}, () => {});
331
+
332
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
298
333
  const timeout = new Promise<ProcessRunResult>((resolve) => {
299
- setTimeout(() => resolve({
334
+ timeoutHandle = setTimeout(() => resolve({
300
335
  code: -1,
301
336
  stdout: "",
302
337
  stderr: "",
@@ -306,9 +341,9 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
306
341
  let runResult: ProcessRunResult;
307
342
  try {
308
343
  runResult = await Promise.race([
309
- runner(options.pythonPath, [options.bridgeScriptPath], { input, timeoutMs: options.timeoutMs }),
344
+ runnerPromise,
310
345
  timeout,
311
- ]);
346
+ ]).finally(() => clearTimeout(timeoutHandle));
312
347
  } catch (error) {
313
348
  return {
314
349
  ok: false,
@@ -335,6 +370,7 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
335
370
  stdoutPreview: "",
336
371
  stderrTail: "",
337
372
  durationMs,
373
+ completion,
338
374
  };
339
375
  }
340
376
 
@@ -427,7 +463,6 @@ export async function setupMempalaceRuntime(
427
463
  const runner = options.runner ?? defaultProcessRunner;
428
464
 
429
465
  // 1. Ensure uv is available (download + verify if needed).
430
- const { ensureUv } = await import("./uv.js");
431
466
  const uv = await ensureUv({
432
467
  managedBinDir: options.managedBinDir,
433
468
  runner,
@@ -550,13 +585,15 @@ export function buildMempalaceCliArgs(
550
585
  const args: string[] = [action];
551
586
  if (action === "split") {
552
587
  if (params.source_file) args.push(params.source_file);
553
- } else if (params.dir) {
588
+ } else if (action !== "repair" && params.dir) {
554
589
  args.push(params.dir);
555
590
  }
556
591
 
557
592
  if (typeof params.limit === "number") args.push("--limit", String(params.limit));
558
593
  if (params.mode) args.push("--mode", params.mode);
594
+ if (action === "repair" && params.source) args.push("--source", params.source);
559
595
  if (params.extract) args.push("--extract");
596
+ if (action === "repair" && params.archive_existing) args.push("--archive-existing");
560
597
  if (params.dry_run) args.push("--dry-run");
561
598
  if (params.include_ignored) args.push("--include-ignored");
562
599
  if (params.no_gitignore) args.push("--no-gitignore");
@@ -1,3 +1,11 @@
1
+ import {
2
+ MEMPALACE_MAX_CONTENT_LENGTH,
3
+ MEMPALACE_MAX_HOPS,
4
+ MEMPALACE_MAX_NAME_LENGTH,
5
+ MEMPALACE_MAX_QUERY_LENGTH,
6
+ MEMPALACE_MAX_RESULTS,
7
+ } from "./upstream-limits.js";
8
+
1
9
  export const MEMPALACE_ACTIONS = [
2
10
  "status",
3
11
  "list_wings",
@@ -35,6 +43,7 @@ export const MEMPALACE_ACTIONS = [
35
43
  "split",
36
44
  "repair",
37
45
  "wake_up",
46
+ "wake_up_and_search",
38
47
  ] as const;
39
48
 
40
49
  export type MempalaceAction = typeof MEMPALACE_ACTIONS[number];
@@ -50,13 +59,14 @@ export interface MempalaceParams {
50
59
  drawer_id?: string;
51
60
  content?: string;
52
61
  source_file?: string;
62
+ source_drawer_id?: string;
53
63
  added_by?: string;
54
64
  subject?: string;
55
65
  predicate?: string;
56
66
  object?: string;
57
67
  valid_from?: string;
58
68
  valid_to?: string;
59
- ended?: boolean;
69
+ ended?: string;
60
70
  as_of?: string;
61
71
  direction?: string;
62
72
  start_room?: string;
@@ -72,8 +82,10 @@ export interface MempalaceParams {
72
82
  topic?: string;
73
83
  dir?: string;
74
84
  mode?: string;
85
+ source?: string;
75
86
  extract?: boolean;
76
87
  dry_run?: boolean;
88
+ archive_existing?: boolean;
77
89
  include_ignored?: boolean;
78
90
  no_gitignore?: boolean;
79
91
  yes?: boolean;
@@ -94,12 +106,14 @@ const STRING_FIELDS = [
94
106
  "drawer_id",
95
107
  "content",
96
108
  "source_file",
109
+ "source_drawer_id",
97
110
  "added_by",
98
111
  "subject",
99
112
  "predicate",
100
113
  "object",
101
114
  "valid_from",
102
115
  "valid_to",
116
+ "ended",
103
117
  "as_of",
104
118
  "direction",
105
119
  "start_room",
@@ -114,34 +128,65 @@ const STRING_FIELDS = [
114
128
  "topic",
115
129
  "dir",
116
130
  "mode",
131
+ "source",
117
132
  ] as const;
118
133
 
119
134
  const POSITIVE_INTEGER_FIELDS = ["limit", "max_hops", "timeout"] as const;
135
+ const MAX_INTEGER_FIELDS: ReadonlyArray<readonly [keyof MempalaceParams, number]> = [
136
+ ["limit", MEMPALACE_MAX_RESULTS],
137
+ ["max_hops", MEMPALACE_MAX_HOPS],
138
+ ];
120
139
  const NON_NEGATIVE_INTEGER_FIELDS = ["offset"] as const;
121
- const BOOLEAN_FIELDS = ["ended", "extract", "dry_run", "include_ignored", "no_gitignore", "yes"] as const;
140
+ const BOOLEAN_FIELDS = ["extract", "dry_run", "archive_existing", "include_ignored", "no_gitignore", "yes"] as const;
141
+
142
+ // Per-field maximum string lengths. Each entry mirrors the JSON schema
143
+ // `maxLength` and corresponds to an upstream MemPalace sanitizer bound:
144
+ // - sanitize_query (tool_search) silently truncates anything beyond
145
+ // MAX_QUERY_LENGTH, so callers can't tell their long query was clipped.
146
+ // - sanitize_name / sanitize_kg_value / sanitize_content all raise
147
+ // ValueError beyond their limits, which surfaces as a domain error
148
+ // from the python bridge instead of at the TS boundary.
149
+ // Enforcing here gives every overlong field the same `field exceeds <N>
150
+ // characters` error path before the python child is spawned.
151
+ const MAX_LENGTH_FIELDS: ReadonlyArray<readonly [keyof MempalaceParams, number]> = [
152
+ ["query", MEMPALACE_MAX_QUERY_LENGTH],
153
+ ["content", MEMPALACE_MAX_CONTENT_LENGTH],
154
+ ["entry", MEMPALACE_MAX_CONTENT_LENGTH],
155
+ ["wing", MEMPALACE_MAX_NAME_LENGTH],
156
+ ["room", MEMPALACE_MAX_NAME_LENGTH],
157
+ ["subject", MEMPALACE_MAX_NAME_LENGTH],
158
+ ["predicate", MEMPALACE_MAX_NAME_LENGTH],
159
+ ["object", MEMPALACE_MAX_NAME_LENGTH],
160
+ ["start_room", MEMPALACE_MAX_NAME_LENGTH],
161
+ ["source_wing", MEMPALACE_MAX_NAME_LENGTH],
162
+ ["source_room", MEMPALACE_MAX_NAME_LENGTH],
163
+ ["target_wing", MEMPALACE_MAX_NAME_LENGTH],
164
+ ["target_room", MEMPALACE_MAX_NAME_LENGTH],
165
+ ["agent_name", MEMPALACE_MAX_NAME_LENGTH],
166
+ ["topic", MEMPALACE_MAX_NAME_LENGTH],
167
+ ] as const;
122
168
 
123
- const REQUIRED_FIELDS: Partial<Record<MempalaceAction, readonly (keyof MempalaceParams)[]>> = {
169
+ export const REQUIRED_FIELDS: Partial<Record<MempalaceAction, readonly (keyof MempalaceParams)[]>> = {
124
170
  search: ["query"],
125
171
  check_duplicate: ["content"],
126
172
  get_drawer: ["drawer_id"],
127
173
  list_drawers: ["wing"],
128
- add_drawer: ["content"],
174
+ add_drawer: ["wing", "room", "content"],
129
175
  update_drawer: ["drawer_id", "content"],
130
176
  delete_drawer: ["drawer_id"],
131
177
  kg_query: ["subject"],
132
178
  kg_add: ["subject", "predicate", "object"],
133
- kg_invalidate: ["subject", "predicate"],
179
+ kg_invalidate: ["subject", "predicate", "object"],
134
180
  kg_timeline: ["subject"],
135
181
  traverse: ["start_room"],
136
- find_tunnels: ["source_wing", "source_room"],
137
182
  create_tunnel: ["source_wing", "source_room", "target_wing", "target_room", "label"],
138
183
  delete_tunnel: ["tunnel_id"],
139
184
  follow_tunnels: ["source_wing", "source_room"],
140
- diary_write: ["entry"],
185
+ diary_write: ["entry", "agent_name"],
186
+ diary_read: ["agent_name"],
141
187
  init: ["dir"],
142
188
  mine: ["dir"],
143
189
  split: ["source_file"],
144
- repair: ["dir"],
145
190
  };
146
191
 
147
192
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -156,6 +201,44 @@ function isNonEmptyString(value: unknown): value is string {
156
201
  return typeof value === "string" && value.trim().length > 0;
157
202
  }
158
203
 
204
+ const ISO_DATE_PART_PATTERN = "\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])";
205
+ const ISO_UTC_DATETIME_PATTERN = `${ISO_DATE_PART_PATTERN}T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|\\+00:00)`;
206
+ const ISO_TEMPORAL_PATTERN = `^(?:${ISO_DATE_PART_PATTERN}|${ISO_UTC_DATETIME_PATTERN})$`;
207
+ const ISO_TEMPORAL_DESCRIPTION =
208
+ "ISO temporal string accepted by MemPalace: YYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ, or YYYY-MM-DDTHH:MM:SS+00:00.";
209
+ const ISO_TEMPORAL_SCHEMA = {
210
+ type: "string",
211
+ pattern: ISO_TEMPORAL_PATTERN,
212
+ description: ISO_TEMPORAL_DESCRIPTION,
213
+ } as const;
214
+ const ISO_TEMPORAL_REGEX = new RegExp(ISO_TEMPORAL_PATTERN);
215
+ const ISO_TEMPORAL_FIELDS = ["as_of", "valid_from", "valid_to", "ended"] as const;
216
+ const DAYS_IN_MONTH = [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const;
217
+
218
+ function fixedInt(value: string, start: number, end: number): number {
219
+ let out = 0;
220
+ for (let i = start; i < end; i += 1) {
221
+ out = out * 10 + value.charCodeAt(i) - 48;
222
+ }
223
+ return out;
224
+ }
225
+
226
+ function isLeapYear(year: number): boolean {
227
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
228
+ }
229
+
230
+ function isValidIsoTemporal(value: string): boolean {
231
+ if (!ISO_TEMPORAL_REGEX.test(value)) return false;
232
+
233
+ const year = fixedInt(value, 0, 4);
234
+ const month = fixedInt(value, 5, 7);
235
+ const day = fixedInt(value, 8, 10);
236
+ if (year < 1) return false;
237
+
238
+ const daysInMonth = month === 2 ? (isLeapYear(year) ? 29 : 28) : DAYS_IN_MONTH[month - 1];
239
+ return day <= daysInMonth;
240
+ }
241
+
159
242
  export const mempalaceToolParameters = {
160
243
  type: "object",
161
244
  additionalProperties: false,
@@ -166,38 +249,45 @@ export const mempalaceToolParameters = {
166
249
  description: "Action to dispatch.",
167
250
  },
168
251
  palace: { type: "string" },
169
- wing: { type: "string" },
170
- room: { type: "string" },
171
- query: { type: "string" },
172
- limit: { type: "integer", minimum: 1 },
173
- offset: { type: "integer", minimum: 0 },
252
+ wing: {
253
+ type: "string",
254
+ maxLength: MEMPALACE_MAX_NAME_LENGTH,
255
+ description: "Wing name. Required for list_drawers to scope the query and avoid an accidental full-palace dump.",
256
+ },
257
+ room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
258
+ query: { type: "string", maxLength: MEMPALACE_MAX_QUERY_LENGTH },
259
+ limit: { type: "integer", minimum: 1, maximum: MEMPALACE_MAX_RESULTS },
260
+ offset: { type: "integer", minimum: 0, description: "Zero-based pagination offset (use with limit for list_drawers and similar listing actions)." },
174
261
  drawer_id: { type: "string" },
175
- content: { type: "string" },
262
+ content: { type: "string", maxLength: MEMPALACE_MAX_CONTENT_LENGTH },
176
263
  source_file: { type: "string" },
264
+ source_drawer_id: { type: "string" },
177
265
  added_by: { type: "string" },
178
- subject: { type: "string" },
179
- predicate: { type: "string" },
180
- object: { type: "string" },
181
- valid_from: { type: "string" },
182
- valid_to: { type: "string" },
183
- ended: { type: "boolean" },
184
- as_of: { type: "string" },
266
+ subject: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
267
+ predicate: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
268
+ object: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
269
+ valid_from: ISO_TEMPORAL_SCHEMA,
270
+ valid_to: ISO_TEMPORAL_SCHEMA,
271
+ ended: ISO_TEMPORAL_SCHEMA,
272
+ as_of: ISO_TEMPORAL_SCHEMA,
185
273
  direction: { type: "string" },
186
- start_room: { type: "string" },
187
- max_hops: { type: "integer", minimum: 1 },
188
- source_wing: { type: "string" },
189
- source_room: { type: "string" },
190
- target_wing: { type: "string" },
191
- target_room: { type: "string" },
274
+ start_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
275
+ max_hops: { type: "integer", minimum: 1, maximum: MEMPALACE_MAX_HOPS },
276
+ source_wing: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
277
+ source_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
278
+ target_wing: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
279
+ target_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
192
280
  label: { type: "string" },
193
281
  tunnel_id: { type: "string" },
194
- agent_name: { type: "string" },
195
- entry: { type: "string" },
196
- topic: { type: "string" },
282
+ agent_name: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
283
+ entry: { type: "string", maxLength: MEMPALACE_MAX_CONTENT_LENGTH },
284
+ topic: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
197
285
  dir: { type: "string" },
198
286
  mode: { type: "string" },
287
+ source: { type: "string" },
199
288
  extract: { type: "boolean" },
200
289
  dry_run: { type: "boolean" },
290
+ archive_existing: { type: "boolean" },
201
291
  include_ignored: { type: "boolean" },
202
292
  no_gitignore: { type: "boolean" },
203
293
  yes: { type: "boolean" },
@@ -254,6 +344,37 @@ export function validateMempalaceParams(params: unknown): MempalaceValidationRes
254
344
  }
255
345
  }
256
346
 
347
+ for (const [field, max] of MAX_INTEGER_FIELDS) {
348
+ const value = params[field];
349
+ if (typeof value === "number" && Number.isInteger(value) && value > max) {
350
+ errors.push(`${field} exceeds maximum of ${max}`);
351
+ }
352
+ }
353
+
354
+ for (const [field, max] of MAX_LENGTH_FIELDS) {
355
+ const value = params[field];
356
+ if (typeof value === "string" && value.length > max) {
357
+ errors.push(`${field} exceeds maximum length of ${max} characters`);
358
+ }
359
+ }
360
+
361
+ // ISO temporal fields: callers commonly pass `" 2026-05-13 "` from logs or
362
+ // env vars — strip surrounding whitespace before validating so we accept
363
+ // the same shape MemPalace's downstream sqlite comparison would. Empty
364
+ // strings are still rejected (semantically not a date, and would be stored
365
+ // as garbage in the kg.triples valid_from/valid_to columns).
366
+ for (const field of ISO_TEMPORAL_FIELDS) {
367
+ const value = params[field];
368
+ if (typeof value !== "string") continue;
369
+ const trimmed = value.trim();
370
+ if (trimmed !== value) {
371
+ (params as Record<string, unknown>)[field] = trimmed;
372
+ }
373
+ if (!isValidIsoTemporal(trimmed)) {
374
+ errors.push(`${field} must be an ISO temporal string accepted by MemPalace`);
375
+ }
376
+ }
377
+
257
378
  for (const field of REQUIRED_FIELDS[action] ?? []) {
258
379
  if (!isNonEmptyString(params[field])) {
259
380
  errors.push(`${field} is required for action ${action}`);
@@ -185,6 +185,11 @@ export function buildCompactionCheckpoint(options: SessionSummaryOptions): Compa
185
185
 
186
186
  export function buildShutdownDiary(options: SessionSummaryOptions): ShutdownDiary {
187
187
  const { body, now } = buildBody("MemPalace shutdown diary", "shutdown", options);
188
+ // source_file convention: the bridge's Python layer (mempalace_bridge.py) embeds
189
+ // the source_file value as a deterministic first-line prefix in the entry text:
190
+ // "[source: <source_file>]\n<entry>"
191
+ // tool_diary_write does not accept source_file natively; the prefix is how the
192
+ // round-trip proves the linkage (diary_read returns the full document text).
188
193
  return {
189
194
  entry: body,
190
195
  metadata: {