create-walle 0.9.13 → 0.9.15

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 (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -2,6 +2,19 @@
2
2
  const { execFile } = require('child_process');
3
3
  const path = require('path');
4
4
 
5
+ function _worktreeNamespace(options) {
6
+ const raw = typeof options === 'string'
7
+ ? options
8
+ : (options && (options.namespace || options.agentType || options.owner)) || '';
9
+ const normalized = String(raw || '').toLowerCase();
10
+ return normalized === 'walle' || normalized === 'wall-e' ? 'walle' : 'claude';
11
+ }
12
+
13
+ function _worktreeParentDir(cwd, options) {
14
+ const namespace = _worktreeNamespace(options);
15
+ return path.join(cwd, namespace === 'walle' ? '.walle' : '.claude', 'worktrees');
16
+ }
17
+
5
18
  // Run a git command in a given project directory
6
19
  function git(cwd, args, maxBuffer = 1024 * 1024 * 5) {
7
20
  return new Promise((resolve, reject) => {
@@ -301,10 +314,15 @@ async function _branchExists(cwd, branch) {
301
314
  // worktree to it (no -b).
302
315
  // - Path exists on disk but unregistered, or path/branch is in use by
303
316
  // a different mapping → auto-suffix the name (-2, -3, …) up to -20.
304
- async function createWorktree(cwd, name, baseBranch) {
317
+ async function createWorktree(cwd, name, baseBranch, options) {
305
318
  const fs = require('fs');
319
+ if (baseBranch && typeof baseBranch === 'object') {
320
+ options = baseBranch;
321
+ baseBranch = undefined;
322
+ }
306
323
  const base = baseBranch || 'HEAD';
307
- const parentDir = path.join(cwd, '.claude', 'worktrees');
324
+ const namespace = _worktreeNamespace(options);
325
+ const parentDir = _worktreeParentDir(cwd, namespace);
308
326
  fs.mkdirSync(parentDir, { recursive: true });
309
327
 
310
328
  const worktrees = await listWorktrees(cwd).catch(() => []);
@@ -315,7 +333,7 @@ async function createWorktree(cwd, name, baseBranch) {
315
333
  const initialReal = _real(initialPath);
316
334
  const existingAtPath = worktrees.find(w => _real(w.path) === initialReal);
317
335
  if (existingAtPath && existingAtPath.branch === name) {
318
- return { path: existingAtPath.path, branch: existingAtPath.branch, reused: true };
336
+ return { path: existingAtPath.path, branch: existingAtPath.branch, reused: true, namespace };
319
337
  }
320
338
 
321
339
  // Attach to existing branch (no clash on path). Common after a worktree
@@ -323,7 +341,7 @@ async function createWorktree(cwd, name, baseBranch) {
323
341
  if (!existingAtPath && !fs.existsSync(initialPath) && await _branchExists(cwd, name)
324
342
  && !worktrees.some(w => w.branch === name)) {
325
343
  await git(cwd, ['worktree', 'add', initialPath, name]);
326
- return { path: initialPath, branch: name, attached: true };
344
+ return { path: initialPath, branch: name, attached: true, namespace };
327
345
  }
328
346
 
329
347
  // Pick a free (path, branch) pair. Suffix until both slots are clear.
@@ -339,6 +357,7 @@ async function createWorktree(cwd, name, baseBranch) {
339
357
  return {
340
358
  path: candidatePath,
341
359
  branch: candidateName,
360
+ namespace,
342
361
  ...(candidateName !== name ? { suffixed: true, requestedName: name } : {}),
343
362
  };
344
363
  }
@@ -437,7 +456,7 @@ function _realpath(p) {
437
456
  //
438
457
  // listRichWorktrees(cwd, mainBranch) returns each worktree augmented with:
439
458
  // - state: 'primary'|'clean'|'ahead'|'behind'|'diverged'|'dirty'|'detached'|'ghost'
440
- // - isCanonical: path lives under <repo>/.claude/worktrees/<name>
459
+ // - isCanonical: path lives under an agent-owned <repo>/.claude|.walle/worktrees/<name>
441
460
  // - isGhost: path contains /~/ corruption OR path doesn't exist on disk
442
461
  // - ahead/behind vs mainBranch
443
462
  // - dirtyFiles: count of modified+untracked
@@ -457,6 +476,106 @@ async function _gitSafe(cwd, args, timeoutMs = 5000) {
457
476
  });
458
477
  }
459
478
 
479
+ function _parseGitHubRemoteUrl(remoteUrl) {
480
+ const raw = String(remoteUrl || '').trim();
481
+ if (!raw) return null;
482
+
483
+ let host = 'github.com';
484
+ let owner = '';
485
+ let repo = '';
486
+ let match = raw.match(/^git@([^:]+):([^/]+)\/(.+)$/);
487
+ if (match) {
488
+ host = match[1];
489
+ owner = match[2];
490
+ repo = match[3];
491
+ } else {
492
+ match = raw.match(/^ssh:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+)$/);
493
+ if (match) {
494
+ host = match[1];
495
+ owner = match[2];
496
+ repo = match[3];
497
+ } else {
498
+ match = raw.match(/^https?:\/\/([^/]+)\/([^/]+)\/(.+)$/);
499
+ if (!match) return null;
500
+ host = match[1];
501
+ owner = match[2];
502
+ repo = match[3];
503
+ }
504
+ }
505
+
506
+ repo = repo.replace(/(?:\.git)?\/?$/, '');
507
+ if (!host || !owner || !repo || repo.includes('/')) return null;
508
+ const nameWithOwner = `${owner}/${repo}`;
509
+ return {
510
+ host,
511
+ owner,
512
+ repo,
513
+ nameWithOwner,
514
+ ghRepo: host === 'github.com' ? nameWithOwner : `${host}/${nameWithOwner}`,
515
+ };
516
+ }
517
+
518
+ async function _githubRemoteSpec(cwd, remoteName) {
519
+ if (!remoteName) return null;
520
+ const urls = [
521
+ await _gitSafe(cwd, ['remote', 'get-url', remoteName]),
522
+ await _gitSafe(cwd, ['remote', 'get-url', '--push', remoteName]),
523
+ ].filter(Boolean);
524
+ for (const url of urls) {
525
+ const spec = _parseGitHubRemoteUrl(url);
526
+ if (spec) return { ...spec, remote: remoteName, url };
527
+ }
528
+ return null;
529
+ }
530
+
531
+ async function _selectPrBaseRemote(cwd, base, remoteNames) {
532
+ const remotes = Array.isArray(remoteNames) ? remoteNames : [];
533
+ if (remotes.includes('upstream') && await _githubRemoteSpec(cwd, 'upstream')) {
534
+ return 'upstream';
535
+ }
536
+
537
+ const configured = await _gitSafe(cwd, ['config', '--get', `branch.${base}.remote`]);
538
+ if (configured && configured !== '.' && remotes.includes(configured)
539
+ && await _githubRemoteSpec(cwd, configured)) {
540
+ return configured;
541
+ }
542
+
543
+ if (remotes.includes('origin') && await _githubRemoteSpec(cwd, 'origin')) {
544
+ return 'origin';
545
+ }
546
+
547
+ for (const remote of remotes) {
548
+ if (await _githubRemoteSpec(cwd, remote)) return remote;
549
+ }
550
+ return null;
551
+ }
552
+
553
+ async function _buildGhPrCreateArgs(cwd, branchName, opts) {
554
+ opts = opts || {};
555
+ const base = opts.base || 'main';
556
+ const title = opts.title || branchName;
557
+ const body = opts.body || `Branch \`${branchName}\` opened.`;
558
+ const remoteOut = await _gitSafe(cwd, ['remote']);
559
+ const remoteNames = remoteOut ? remoteOut.split('\n').map(s => s.trim()).filter(Boolean) : [];
560
+ const pushRemote = remoteNames.includes('origin') ? 'origin' : remoteNames[0];
561
+ const baseRemote = await _selectPrBaseRemote(cwd, base, remoteNames);
562
+ const baseSpec = await _githubRemoteSpec(cwd, baseRemote);
563
+ const pushSpec = await _githubRemoteSpec(cwd, pushRemote);
564
+
565
+ let head = branchName;
566
+ if (baseSpec && pushSpec
567
+ && (baseSpec.host !== pushSpec.host
568
+ || baseSpec.owner !== pushSpec.owner
569
+ || baseSpec.repo !== pushSpec.repo)) {
570
+ head = `${pushSpec.owner}:${branchName}`;
571
+ }
572
+
573
+ const args = ['pr', 'create'];
574
+ if (baseSpec) args.push('--repo', baseSpec.ghRepo);
575
+ args.push('--base', base, '--head', head, '--title', title, '--body', body);
576
+ return { args, baseRemote, pushRemote, baseSpec, pushSpec, head };
577
+ }
578
+
460
579
  function _isGhostPath(p) {
461
580
  if (!p) return true;
462
581
  if (p.includes('/~/')) return true; // literal-tilde corruption
@@ -465,8 +584,10 @@ function _isGhostPath(p) {
465
584
 
466
585
  function _isCanonicalPath(repoRoot, p, isMain) {
467
586
  if (isMain) return true;
468
- const expected = path.join(repoRoot, '.claude', 'worktrees') + path.sep;
469
- return p.startsWith(expected);
587
+ const value = String(p || '');
588
+ const expectedClaude = path.join(repoRoot, '.claude', 'worktrees') + path.sep;
589
+ const expectedWalle = path.join(repoRoot, '.walle', 'worktrees') + path.sep;
590
+ return value.startsWith(expectedClaude) || value.startsWith(expectedWalle);
470
591
  }
471
592
 
472
593
  function _classifyState(wt) {
@@ -949,11 +1070,18 @@ async function pushAndCreatePR(cwd, branchName, opts) {
949
1070
  body = log ? `## Summary\n\n${log}\n` : `Branch \`${branchName}\` opened.`;
950
1071
  }
951
1072
 
952
- // gh pr create use --json to get the URL back, fall back to scraping stdout.
1073
+ // gh pr create. Do not let gh infer the repository from `origin`: in forked
1074
+ // or stale-remote checkouts that can point it at an inaccessible repo.
1075
+ const prCommand = await _buildGhPrCreateArgs(wt.path, branchName, { base, title, body });
953
1076
  const ghOut = await new Promise((resolve, reject) => {
954
- execFile('gh', ['pr', 'create', '--base', base, '--head', branchName, '--title', title, '--body', body],
1077
+ execFile('gh', prCommand.args,
955
1078
  { cwd: wt.path, timeout: 60000 }, (err, stdout, stderr) => {
956
- if (err) return reject(new Error(`gh pr create failed: ${stderr || err.message}`));
1079
+ if (err) {
1080
+ const target = prCommand.baseSpec
1081
+ ? `${prCommand.baseSpec.ghRepo} from ${prCommand.head}`
1082
+ : `the repository inferred by gh from ${wt.path}`;
1083
+ return reject(new Error(`gh pr create failed for ${target}: ${stderr || err.message}`));
1084
+ }
957
1085
  resolve((stdout || '').trim());
958
1086
  });
959
1087
  });
@@ -971,4 +1099,6 @@ module.exports = {
971
1099
  // Internal helpers exposed for tests
972
1100
  _classifyState, _buildSummary, _isGhostPath, _isCanonicalPath, STATE_SORT_RANK,
973
1101
  _classifyMainRemote, _recommendedAction, _syncAllEligibility, _sameWorktreePath,
1102
+ _parseGitHubRemoteUrl, _githubRemoteSpec, _buildGhPrCreateArgs,
1103
+ _worktreeNamespace, _worktreeParentDir,
974
1104
  };
@@ -44,7 +44,7 @@ const AGENT_CAPABILITIES = Object.freeze({
44
44
  liveStatus: true,
45
45
  stateDetector: false,
46
46
  structuredTranscript: true,
47
- promptNavigation: 'none',
47
+ promptNavigation: 'chat',
48
48
  review: true,
49
49
  resume: false,
50
50
  resumeSpec: () => null,
@@ -101,6 +101,35 @@ function getPreset(presetId) {
101
101
  return PRESETS[presetId] || null;
102
102
  }
103
103
 
104
+ const INHERITED_AGENT_ENV_KEYS = [
105
+ 'CLAUDECODE',
106
+ 'CLAUDE_CODE',
107
+ 'CLAUDE_CODE_ENTRYPOINT',
108
+ 'CLAUDE_CODE_ENABLE_TELEMETRY',
109
+ 'CODEX_THREAD_ID',
110
+ 'CODEX_SESSION_ID',
111
+ 'CODEX_TURN_ID',
112
+ 'CODEX_CI',
113
+ 'CODEX_SANDBOX_NETWORK_DISABLED',
114
+ 'GEMINI_SESSION_ID',
115
+ ];
116
+
117
+ function sanitizeInheritedAgentEnv(env) {
118
+ if (!env || typeof env !== 'object') return env;
119
+ for (const key of INHERITED_AGENT_ENV_KEYS) delete env[key];
120
+ return env;
121
+ }
122
+
123
+ function buildResourceAttributes(existing, ctmSessionId) {
124
+ const prior = String(existing || '')
125
+ .split(',')
126
+ .map(s => s.trim())
127
+ .filter(Boolean)
128
+ .filter(s => !/^ctm\.session_id=/.test(s) && !/^clideck\.session_id=/.test(s));
129
+ prior.push(`ctm.session_id=${ctmSessionId}`);
130
+ return prior.join(',');
131
+ }
132
+
104
133
  /**
105
134
  * Build the env object to inject at pty.spawn time.
106
135
  * - ctmSessionId: CTM's UUID for the session (goes into OTEL_RESOURCE_ATTRIBUTES)
@@ -109,21 +138,25 @@ function getPreset(presetId) {
109
138
  * Returns an object that can be spread into the env option: `env: { ...process.env, ...telemetryEnv }`.
110
139
  * Always includes CTM_SESSION_ID and CTM_PORT — hook bins read these from their own env.
111
140
  */
112
- function buildTelemetryEnv(ctmSessionId, cmd, port) {
141
+ function buildTelemetryEnv(ctmSessionId, cmd, port, baseEnv = process.env) {
113
142
  const presetId = detectPresetId(cmd);
114
143
  const preset = presetId ? PRESETS[presetId] : null;
115
144
  const env = {
116
145
  CTM_SESSION_ID: ctmSessionId,
117
146
  CTM_PORT: String(port),
147
+ OTEL_RESOURCE_ATTRIBUTES: buildResourceAttributes(baseEnv?.OTEL_RESOURCE_ATTRIBUTES, ctmSessionId),
118
148
  };
119
149
  if (!preset || !preset.telemetryEnv) return env;
120
150
  for (const [k, v] of Object.entries(preset.telemetryEnv)) {
121
151
  env[k] = String(v).replace('{{port}}', String(port));
122
152
  }
123
- // Correlate OTLP log records to the CTM session id
124
- const existing = process.env.OTEL_RESOURCE_ATTRIBUTES || '';
125
- env.OTEL_RESOURCE_ATTRIBUTES = (existing ? existing + ',' : '') + `ctm.session_id=${ctmSessionId}`;
126
153
  return env;
127
154
  }
128
155
 
129
- module.exports = { PRESETS, detectPresetId, getPreset, buildTelemetryEnv };
156
+ module.exports = {
157
+ PRESETS,
158
+ detectPresetId,
159
+ getPreset,
160
+ buildTelemetryEnv,
161
+ sanitizeInheritedAgentEnv,
162
+ };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function stripAnsi(text) {
6
+ return String(text || '')
7
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
8
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
9
+ }
10
+
11
+ function normalizeForTerminalSearch(text) {
12
+ return stripAnsi(text)
13
+ .normalize('NFKC')
14
+ .replace(/[*_`#>~]/g, '')
15
+ .replace(/[\x00-\x1f\x7f]/g, ' ')
16
+ .replace(/\s+/g, ' ')
17
+ .trim()
18
+ .toLowerCase();
19
+ }
20
+
21
+ function meaningfulLines(text) {
22
+ return stripAnsi(text)
23
+ .split(/\r?\n/)
24
+ .map((line) => line.trim())
25
+ .filter(Boolean);
26
+ }
27
+
28
+ function markerForFinalText(text) {
29
+ const lines = meaningfulLines(text);
30
+ if (lines.length === 0) return '';
31
+ let candidate = lines.find((line) => normalizeForTerminalSearch(line).length >= 24) || lines[0];
32
+ if (normalizeForTerminalSearch(candidate).length < 24 && lines.length > 1) {
33
+ candidate = `${candidate} ${lines[1]}`;
34
+ }
35
+ return normalizeForTerminalSearch(candidate).slice(0, 180);
36
+ }
37
+
38
+ function terminalContainsFinalText(terminalText, finalText) {
39
+ const marker = markerForFinalText(finalText);
40
+ if (!marker) return true;
41
+ return normalizeForTerminalSearch(terminalText).includes(marker);
42
+ }
43
+
44
+ function fingerprintFinalText(text) {
45
+ return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16);
46
+ }
47
+
48
+ module.exports = {
49
+ fingerprintFinalText,
50
+ markerForFinalText,
51
+ normalizeForTerminalSearch,
52
+ terminalContainsFinalText,
53
+ };
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ let ctmSessionContext = null;
4
+ let ctmSessionContextLoadError = null;
5
+
6
+ function loadCtmSessionContext() {
7
+ if (ctmSessionContext || ctmSessionContextLoadError) return ctmSessionContext;
8
+ try {
9
+ ctmSessionContext = require('../../wall-e/memory/ctm-session-context');
10
+ } catch (err) {
11
+ ctmSessionContextLoadError = err;
12
+ }
13
+ return ctmSessionContext;
14
+ }
15
+
16
+ function handleCtmSessionContextApi(req, res, url, { db, dbModule } = {}) {
17
+ if (!url.pathname.startsWith('/api/ctm/session-memory')) return false;
18
+
19
+ const context = loadCtmSessionContext();
20
+ if (!context) {
21
+ return respondJson(res, 503, {
22
+ ok: false,
23
+ source: 'ctm-api',
24
+ reason: 'ctm_session_context_unavailable',
25
+ error: ctmSessionContextLoadError?.message || 'CTM session context module unavailable',
26
+ });
27
+ }
28
+
29
+ try {
30
+ if (url.pathname === '/api/ctm/session-memory/health' && req.method === 'GET') {
31
+ return respondJson(res, 200, asApiResult(context.getCtmDbHealth({ db })));
32
+ }
33
+
34
+ if (url.pathname === '/api/ctm/session-memory/search' && req.method === 'GET') {
35
+ const query = url.searchParams.get('q') || url.searchParams.get('query') || '';
36
+ const limit = url.searchParams.get('limit') || undefined;
37
+ return respondJson(res, 200, asApiResult(context.searchCtmSessions({ query, limit, db })));
38
+ }
39
+
40
+ if (url.pathname === '/api/ctm/session-memory/context' && req.method === 'GET') {
41
+ return respondJson(res, 200, asApiResult(context.getCtmSessionContext({
42
+ session_id: url.searchParams.get('session_id') || url.searchParams.get('id') || '',
43
+ session_ids: parseListParams(url.searchParams, 'session_id', 'session_ids', 'ids'),
44
+ limit: url.searchParams.get('limit') || undefined,
45
+ cursor: url.searchParams.get('cursor') || undefined,
46
+ include_raw: parseBool(url.searchParams.get('include_raw')),
47
+ dedupe: !parseBool(url.searchParams.get('no_dedupe')),
48
+ format: url.searchParams.get('format') || 'messages',
49
+ db,
50
+ })));
51
+ }
52
+
53
+ if (
54
+ (url.pathname === '/api/ctm/session-memory/message-index/backfill'
55
+ || url.pathname === '/api/ctm/session-memory/rebuild-message-index')
56
+ && req.method === 'POST'
57
+ ) {
58
+ readJsonBody(req, 32 * 1024)
59
+ .then((body) => handleMessageIndexBackfill({ body, res, context, db, dbModule })
60
+ .catch((err) => respondJson(res, 500, {
61
+ ok: false,
62
+ source: 'ctm-api',
63
+ authority: 'ctm',
64
+ reason: 'ctm_message_index_backfill_failed',
65
+ error: err.message,
66
+ })))
67
+ .catch((err) => respondJson(res, 400, {
68
+ ok: false,
69
+ source: 'ctm-api',
70
+ reason: 'invalid_json',
71
+ error: err.message,
72
+ }));
73
+ return true;
74
+ }
75
+
76
+ if (url.pathname === '/api/ctm/session-memory/context-pack' && req.method === 'POST') {
77
+ readJsonBody(req, 128 * 1024)
78
+ .then((body) => {
79
+ respondJson(res, 200, asApiResult(context.buildContextPack({
80
+ task: body.task || '',
81
+ query: body.query || '',
82
+ session_ids: body.session_ids || body.ids,
83
+ limit: body.limit,
84
+ token_budget: body.token_budget,
85
+ include_raw: Boolean(body.include_raw),
86
+ mode: body.mode || 'auto',
87
+ db,
88
+ })));
89
+ })
90
+ .catch((err) => respondJson(res, 400, {
91
+ ok: false,
92
+ source: 'ctm-api',
93
+ reason: 'invalid_json',
94
+ error: err.message,
95
+ }));
96
+ return true;
97
+ }
98
+
99
+ return respondJson(res, 404, {
100
+ ok: false,
101
+ source: 'ctm-api',
102
+ reason: 'not_found',
103
+ });
104
+ } catch (err) {
105
+ return respondJson(res, 500, {
106
+ ok: false,
107
+ source: 'ctm-api',
108
+ reason: 'ctm_session_context_failed',
109
+ error: err.message,
110
+ });
111
+ }
112
+ }
113
+
114
+ async function handleMessageIndexBackfill({ body, res, context, db, dbModule }) {
115
+ const limit = clampInt(body.limit, 100, 1, 1000);
116
+ const chunkSize = clampInt(body.chunk_size ?? body.chunkSize, 250, 25, 2000);
117
+ const dryRun = parseBool(body.dry_run);
118
+ if (dryRun) {
119
+ return respondJson(res, 200, asApiResult(context.backfillCtmSessionMessages({
120
+ db,
121
+ limit,
122
+ dry_run: true,
123
+ })));
124
+ }
125
+
126
+ if (!dbModule || typeof dbModule.backfillSessionMessagesAsync !== 'function') {
127
+ return respondJson(res, 503, {
128
+ ok: false,
129
+ source: 'ctm-api',
130
+ authority: 'ctm',
131
+ reason: 'ctm_message_index_writer_unavailable',
132
+ });
133
+ }
134
+
135
+ const insertedMessages = await dbModule.backfillSessionMessagesAsync({ limit, chunkSize });
136
+ return respondJson(res, 200, asApiResult({
137
+ ok: true,
138
+ source: 'ctm-db',
139
+ dry_run: false,
140
+ limit,
141
+ chunk_size: chunkSize,
142
+ inserted_messages: insertedMessages,
143
+ table_counts: tableCounts(db, ['session_conversations', 'session_messages', 'session_messages_fts']),
144
+ }));
145
+ }
146
+
147
+ function asApiResult(result) {
148
+ return {
149
+ ...(result || {}),
150
+ source: 'ctm-api',
151
+ backend_source: result?.source || 'ctm-db',
152
+ authority: 'ctm',
153
+ };
154
+ }
155
+
156
+ function parseListParams(searchParams, ...names) {
157
+ const values = [];
158
+ for (const name of names) {
159
+ for (const value of searchParams.getAll(name)) {
160
+ values.push(...String(value || '').split(','));
161
+ }
162
+ }
163
+ return values.map((value) => value.trim()).filter(Boolean);
164
+ }
165
+
166
+ function parseBool(value) {
167
+ return value === true || value === 1 || value === '1' || value === 'true' || value === 'yes';
168
+ }
169
+
170
+ function clampInt(value, fallback, min, max) {
171
+ const n = Number(value);
172
+ if (!Number.isFinite(n)) return fallback;
173
+ return Math.min(Math.max(Math.trunc(n), min), max);
174
+ }
175
+
176
+ function tableCounts(db, names) {
177
+ const counts = {};
178
+ if (!db) return counts;
179
+ for (const name of names) {
180
+ try {
181
+ const exists = db.prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?").get(name);
182
+ if (!exists) continue;
183
+ counts[name] = db.prepare(`SELECT COUNT(*) AS count FROM ${name}`).get().count;
184
+ } catch (err) {
185
+ counts._errors = counts._errors || {};
186
+ counts._errors[name] = err.message;
187
+ }
188
+ }
189
+ return counts;
190
+ }
191
+
192
+ function readJsonBody(req, maxBytes) {
193
+ return new Promise((resolve, reject) => {
194
+ let body = '';
195
+ req.on('data', (chunk) => {
196
+ body += chunk;
197
+ if (body.length > maxBytes) {
198
+ reject(new Error('request_body_too_large'));
199
+ req.destroy();
200
+ }
201
+ });
202
+ req.on('end', () => {
203
+ try {
204
+ resolve(body ? JSON.parse(body) : {});
205
+ } catch (err) {
206
+ reject(err);
207
+ }
208
+ });
209
+ req.on('error', reject);
210
+ });
211
+ }
212
+
213
+ function respondJson(res, status, payload) {
214
+ res.writeHead(status, { 'Content-Type': 'application/json' });
215
+ res.end(JSON.stringify(payload));
216
+ return true;
217
+ }
218
+
219
+ module.exports = {
220
+ handleCtmSessionContextApi,
221
+ loadCtmSessionContext,
222
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ function createSessionDiagnostics({ limit = 80, clock = () => Date.now() } = {}) {
4
+ const rings = new Map();
5
+ const cap = Math.max(1, Math.min(Number(limit) || 80, 500));
6
+
7
+ function record(sessionId, event, details = {}) {
8
+ const id = String(sessionId || '').trim();
9
+ if (!id) return null;
10
+ const entry = {
11
+ ts: clock(),
12
+ event: String(event || 'event'),
13
+ ...sanitizeDetails(details),
14
+ };
15
+ const ring = rings.get(id) || [];
16
+ ring.push(entry);
17
+ while (ring.length > cap) ring.shift();
18
+ rings.set(id, ring);
19
+ return entry;
20
+ }
21
+
22
+ function get(sessionId) {
23
+ const id = String(sessionId || '').trim();
24
+ return [...(rings.get(id) || [])];
25
+ }
26
+
27
+ function clear(sessionId) {
28
+ rings.delete(String(sessionId || '').trim());
29
+ }
30
+
31
+ return { record, get, clear };
32
+ }
33
+
34
+ function sanitizeDetails(details) {
35
+ const out = {};
36
+ for (const [key, value] of Object.entries(details || {})) {
37
+ if (value == null) continue;
38
+ if (typeof value === 'string') {
39
+ out[key] = value.length > 500 ? `${value.slice(0, 497)}...` : value;
40
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
41
+ out[key] = value;
42
+ } else if (Array.isArray(value)) {
43
+ out[key] = value.slice(0, 20).map((item) => (
44
+ typeof item === 'string' && item.length > 200 ? `${item.slice(0, 197)}...` : item
45
+ ));
46
+ } else if (typeof value === 'object') {
47
+ out[key] = sanitizeDetails(value);
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ module.exports = {
54
+ createSessionDiagnostics,
55
+ sanitizeDetails,
56
+ };