@ekkos/cli 1.2.14 โ†’ 1.2.16

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.
File without changes
@@ -439,7 +439,7 @@ let proxyModeEnabled = true;
439
439
  /**
440
440
  * Generate a unique session UUID and convert to human-readable name
441
441
  * Each CLI invocation gets a NEW session (not tied to project path)
442
- * Uses uuidToWords from state.ts for consistency with hooks
442
+ * Uses uuidToWords from state.ts for consistency across ekkOS components
443
443
  */
444
444
  function generateCliSessionName() {
445
445
  const sessionUuid = crypto.randomUUID();
@@ -469,12 +469,12 @@ function getEkkosEnv() {
469
469
  env.EKKOS_PROXY_MODE = '1';
470
470
  // Enable ultra-minimal mode by default (30%โ†’20% eviction for constant-cost infinite context)
471
471
  env.EKKOS_ULTRA_MINIMAL = '1';
472
- // Use placeholder for session name - will be bound by hook with Claude's real session
473
- // This fixes the mismatch where CLI generated one name but Claude Code used another
474
- // The hook calls POST /proxy/session/bind with Claude's actual session name
472
+ // Use a unique pending scope until Claude exposes its real session UUID.
473
+ // run.ts binds this scope to the real session immediately on transcript detect.
475
474
  if (!cliSessionName) {
476
- cliSessionName = '_pending'; // Placeholder - hook will bind real name
477
- cliSessionId = `pending-${Date.now()}`;
475
+ const pendingToken = crypto.randomBytes(8).toString('hex');
476
+ cliSessionName = `_pending-${pendingToken}`;
477
+ cliSessionId = `pending-${pendingToken}`;
478
478
  console.log(chalk_1.default.gray(` ๐Ÿ“‚ Session: pending (will bind to Claude session)`));
479
479
  }
480
480
  // Get full userId from config (NOT the truncated version from auth token)
@@ -1085,7 +1085,7 @@ async function run(options) {
1085
1085
  }
1086
1086
  // Check PTY availability early (deterministic, no async race)
1087
1087
  // Windows: SKIP node-pty entirely. ConPTY corrupts Ink's cursor management sequences
1088
- // (hide/show/move), causing ghost โ–Š block cursor artifacts. Use stdio:'inherit' spawn
1088
+ // (hide/show/move), causing ghost block cursor artifacts. Use stdio:'inherit' spawn
1089
1089
  // instead so Ink gets direct terminal access. Hooks still handle session tracking.
1090
1090
  const loadedPty = isWindows ? null : await loadPty();
1091
1091
  const usePty = loadedPty !== null;
@@ -1496,13 +1496,17 @@ async function run(options) {
1496
1496
  const sessionId = file.replace('.jsonl', '');
1497
1497
  transcriptPath = fullPath;
1498
1498
  currentSessionId = sessionId;
1499
+ currentSession = (0, state_1.uuidToWords)(sessionId);
1500
+ (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
1501
+ (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
1502
+ bindRealSessionToProxy(currentSession, 'fast-transcript', currentSessionId);
1499
1503
  dlog(`[TRANSCRIPT] FAST DETECT: New transcript found! ${fullPath}`);
1500
1504
  evictionDebugLog('TRANSCRIPT_SET', 'Fast poll detected new file', {
1501
1505
  transcriptPath,
1502
1506
  currentSessionId,
1503
1507
  elapsedMs: Date.now() - launchTime
1504
1508
  });
1505
- startStreamTailer(transcriptPath, currentSessionId);
1509
+ startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
1506
1510
  // Stop polling
1507
1511
  if (transcriptPollInterval) {
1508
1512
  clearInterval(transcriptPollInterval);
@@ -1516,10 +1520,6 @@ async function run(options) {
1516
1520
  // Project dir doesn't exist yet, keep polling
1517
1521
  }
1518
1522
  }
1519
- // Start polling immediately
1520
- transcriptPollInterval = setInterval(pollForNewTranscript, 500);
1521
- pollForNewTranscript(); // Also run once immediately
1522
- dlog('[TRANSCRIPT] Fast polling started - looking for new jsonl files');
1523
1523
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1524
1524
  // SESSION NAME TRACKING (from live TUI output)
1525
1525
  // Claude prints: "ยท Turn N ยท groovy-koala-saves ยท ๐Ÿ“…"
@@ -1561,6 +1561,10 @@ async function run(options) {
1561
1561
  let turnEndTimeout = null;
1562
1562
  const TURN_END_STABLE_MS = 500; // Must see idle prompt for 500ms
1563
1563
  let pendingClearAfterEviction = false; // Flag to trigger /clear after eviction
1564
+ // Start polling after session-tracking state is initialized.
1565
+ transcriptPollInterval = setInterval(pollForNewTranscript, 500);
1566
+ pollForNewTranscript(); // Also run once immediately
1567
+ dlog('[TRANSCRIPT] Fast polling started - looking for new jsonl files');
1564
1568
  // Debug log to eviction-debug.log for 400 error diagnosis
1565
1569
  function evictionDebugLog(category, msg, data) {
1566
1570
  try {
@@ -1615,22 +1619,30 @@ async function run(options) {
1615
1619
  dlog(`[TRANSCRIPT] Resolved by session ID (${source}): ${candidate}`);
1616
1620
  startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
1617
1621
  }
1618
- function bindRealSessionToProxy(sessionName, source) {
1622
+ function bindRealSessionToProxy(sessionName, source, sessionIdHint) {
1619
1623
  if (!proxyModeEnabled)
1620
1624
  return;
1621
1625
  if (!sessionName || sessionName === '_pending')
1622
1626
  return;
1623
1627
  if (boundProxySession === sessionName || bindingSessionInFlight === sessionName)
1624
1628
  return;
1629
+ const pendingSession = cliSessionName?.startsWith('_pending') ? cliSessionName : undefined;
1630
+ const bindSessionId = sessionIdHint || currentSessionId || undefined;
1625
1631
  bindingSessionInFlight = sessionName;
1626
1632
  void (async () => {
1627
1633
  const maxAttempts = 3;
1628
1634
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1629
- const success = await (0, session_binding_1.bindSession)(sessionName, process.cwd());
1635
+ const success = await (0, session_binding_1.bindSession)({
1636
+ realSession: sessionName,
1637
+ projectPath: process.cwd(),
1638
+ pendingSession,
1639
+ sessionId: bindSessionId,
1640
+ });
1630
1641
  if (success) {
1631
1642
  boundProxySession = sessionName;
1632
1643
  bindingSessionInFlight = null;
1633
1644
  cliSessionName = sessionName;
1645
+ cliSessionId = bindSessionId || cliSessionId;
1634
1646
  dlog(`[SESSION_BIND] Bound ${sessionName} from ${source} (attempt ${attempt}/${maxAttempts})`);
1635
1647
  return;
1636
1648
  }
@@ -2329,7 +2341,7 @@ async function run(options) {
2329
2341
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
2330
2342
  dlog(`Session detected from UUID: ${currentSession}`);
2331
2343
  resolveTranscriptFromSessionId('session-id-from-output');
2332
- bindRealSessionToProxy(currentSession, 'session-id-from-output');
2344
+ bindRealSessionToProxy(currentSession, 'session-id-from-output', currentSessionId || undefined);
2333
2345
  }
2334
2346
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2335
2347
  // SESSION NAME DETECTION (PRIMARY METHOD)
@@ -2363,7 +2375,7 @@ async function run(options) {
2363
2375
  // Also update global state for backwards compatibility
2364
2376
  (0, state_1.updateState)({ sessionName: currentSession });
2365
2377
  dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
2366
- bindRealSessionToProxy(currentSession, 'status-line');
2378
+ bindRealSessionToProxy(currentSession, 'status-line', currentSessionId || undefined);
2367
2379
  resolveTranscriptFromSessionId('status-line');
2368
2380
  }
2369
2381
  }
@@ -2371,7 +2383,7 @@ async function run(options) {
2371
2383
  // Same session, just update timestamp
2372
2384
  lastSeenSessionAt = Date.now();
2373
2385
  if (boundProxySession !== detectedSession) {
2374
- bindRealSessionToProxy(detectedSession, 'status-line-refresh');
2386
+ bindRealSessionToProxy(detectedSession, 'status-line-refresh', currentSessionId || undefined);
2375
2387
  }
2376
2388
  }
2377
2389
  }
File without changes
package/dist/index.js CHANGED
@@ -51,6 +51,9 @@ const agent_1 = require("./commands/agent");
51
51
  const state_1 = require("./utils/state");
52
52
  const index_1 = require("./commands/usage/index");
53
53
  const dashboard_1 = require("./commands/dashboard");
54
+ const swarm_1 = require("./commands/swarm");
55
+ const swarm_dashboard_1 = require("./commands/swarm-dashboard");
56
+ const swarm_setup_1 = require("./commands/swarm-setup");
54
57
  const chalk_1 = __importDefault(require("chalk"));
55
58
  const fs = __importStar(require("fs"));
56
59
  const path = __importStar(require("path"));
@@ -76,7 +79,7 @@ commander_1.program
76
79
  ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run -b')} ${chalk_1.default.gray('Launch with bypass permissions mode')}`,
77
80
  ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos doctor --fix')} ${chalk_1.default.gray('Check and auto-fix system prerequisites')}`,
78
81
  ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos usage daily')} ${chalk_1.default.gray("View today's token usage and costs")}`,
79
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos sessions')} ${chalk_1.default.gray('List active Claude Code sessions')}`,
82
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos swarm launch -t "build X"')} ${chalk_1.default.gray('Launch parallel workers on a task')}`,
80
83
  '',
81
84
  chalk_1.default.gray(' Run ') + chalk_1.default.white('ekkos <command> --help') + chalk_1.default.gray(' for detailed options on any command.'),
82
85
  '',
@@ -154,7 +157,7 @@ commander_1.program
154
157
  title: 'Running',
155
158
  icon: 'โ–ธ',
156
159
  commands: [
157
- { name: 'run', desc: 'Start Claude Code with ekkOS memory', note: 'default' },
160
+ { name: 'run', desc: 'Launch Claude Code with ekkOS memory + auto-continue', note: 'default' },
158
161
  { name: 'test-claude', desc: 'Launch Claude with proxy only (no ccDNA/PTY) for debugging' },
159
162
  { name: 'sessions', desc: 'List active Claude Code sessions' },
160
163
  ],
@@ -177,6 +180,13 @@ commander_1.program
177
180
  { name: 'agent', desc: 'Manage the remote terminal agent (start, stop, status, logs)' },
178
181
  ],
179
182
  },
183
+ {
184
+ title: 'Swarm (Multi-Agent)',
185
+ icon: 'โ–ธ',
186
+ commands: [
187
+ { name: 'swarm', desc: 'Parallel workers, Q-learning routing, swarm dashboard' },
188
+ ],
189
+ },
180
190
  ];
181
191
  const padCmd = 18;
182
192
  let output = '';
@@ -229,7 +239,7 @@ commander_1.program
229
239
  // Run command - launches Claude with auto-continue wrapper
230
240
  commander_1.program
231
241
  .command('run')
232
- .description('Start Claude Code with ekkOS memory (default command)')
242
+ .description('Launch Claude Code with auto-continue (auto /clear + /continue when context is high)')
233
243
  .option('-s, --session <name>', 'Session name to restore on clear')
234
244
  .option('-b, --bypass', 'Enable bypass permissions mode (dangerously skip all permission checks)')
235
245
  .option('-v, --verbose', 'Show debug output')
@@ -342,10 +352,10 @@ hooksCmd
342
352
  (0, index_1.registerUsageCommand)(commander_1.program);
343
353
  // Dashboard command - live TUI for monitoring session usage
344
354
  commander_1.program.addCommand(dashboard_1.dashboardCommand);
345
- // Sessions command - list active Claude Code sessions
355
+ // Sessions command - list active Claude Code sessions (swarm support)
346
356
  commander_1.program
347
357
  .command('sessions')
348
- .description('List active Claude Code sessions')
358
+ .description('List active Claude Code sessions (for swarm/multi-session support)')
349
359
  .option('-j, --json', 'Output machine-readable JSON')
350
360
  .action((options) => {
351
361
  const sessions = (0, state_1.getActiveSessions)();
@@ -470,6 +480,59 @@ agentCmd
470
480
  .action((options) => {
471
481
  (0, agent_1.agentHealth)({ json: options.json });
472
482
  });
483
+ // Swarm command - manage Q-learning routing
484
+ const swarmCmd = commander_1.program
485
+ .command('swarm')
486
+ .description('Manage Swarm Q-learning model routing');
487
+ swarmCmd
488
+ .command('status')
489
+ .description('Show Q-table stats (states, visits, epsilon, top actions)')
490
+ .action(swarm_1.swarmStatus);
491
+ swarmCmd
492
+ .command('reset')
493
+ .description('Clear Q-table from Redis (routing reverts to static rules)')
494
+ .action(swarm_1.swarmReset);
495
+ swarmCmd
496
+ .command('export')
497
+ .description('Export Q-table to .swarm/q-learning-model.json')
498
+ .action(swarm_1.swarmExport);
499
+ swarmCmd
500
+ .command('import')
501
+ .description('Import Q-table from .swarm/q-learning-model.json into Redis')
502
+ .action(swarm_1.swarmImport);
503
+ swarmCmd
504
+ .command('launch')
505
+ .description('Launch parallel workers on a decomposed task (opens wizard if --task is omitted)')
506
+ .option('-w, --workers <count>', 'Number of parallel workers (2-8)', parseInt)
507
+ .option('-t, --task <task>', 'Task description to decompose and execute')
508
+ .option('--no-bypass', 'Disable bypass permissions mode')
509
+ .option('--no-decompose', 'Skip AI decomposition (send same task to all workers)')
510
+ .option('--no-queen', 'Skip launching the Python Queen coordinator')
511
+ .option('--queen-strategy <strategy>', 'Queen strategy (adaptive-default, hierarchical-cascade, mesh-consensus)')
512
+ .option('-v, --verbose', 'Show debug output')
513
+ .action((options) => {
514
+ // Auto-open wizard when --task is missing
515
+ if (!options.task) {
516
+ (0, swarm_setup_1.swarmSetup)();
517
+ return;
518
+ }
519
+ (0, swarm_1.swarmLaunch)({
520
+ workers: options.workers || 4,
521
+ task: options.task,
522
+ bypass: options.bypass !== false,
523
+ noDecompose: options.decompose === false,
524
+ noQueen: options.queen === false,
525
+ queenStrategy: options.queenStrategy,
526
+ verbose: options.verbose,
527
+ });
528
+ });
529
+ swarmCmd
530
+ .command('setup')
531
+ .description('Interactive TUI wizard for configuring and launching a swarm')
532
+ .action(() => {
533
+ (0, swarm_setup_1.swarmSetup)();
534
+ });
535
+ swarmCmd.addCommand(swarm_dashboard_1.swarmDashboardCommand);
473
536
  // Handle `-help` (single dash) โ€” rewrite to `--help` for Commander compatibility
474
537
  const helpIdx = process.argv.indexOf('-help');
475
538
  if (helpIdx !== -1) {
@@ -1,5 +1,12 @@
1
+ interface BindSessionPayload {
2
+ realSession: string;
3
+ projectPath: string;
4
+ pendingSession?: string;
5
+ sessionId?: string;
6
+ }
1
7
  /**
2
8
  * Bind the real session name to the proxy
3
- * This replaces the '_pending' placeholder for this user, enabling proper eviction
9
+ * This replaces the pending placeholder for this user, enabling proper eviction.
4
10
  */
5
- export declare function bindSession(realSession: string, projectPath: string): Promise<boolean>;
11
+ export declare function bindSession({ realSession, projectPath, pendingSession, sessionId, }: BindSessionPayload): Promise<boolean>;
12
+ export {};
@@ -3,42 +3,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.bindSession = bindSession;
4
4
  const state_1 = require("./state");
5
5
  const MEMORY_API_URL = process.env.EKKOS_PROXY_URL || 'https://proxy.ekkos.dev';
6
+ function extractUserIdFromToken(token) {
7
+ const normalized = token.trim();
8
+ if (!normalized)
9
+ return undefined;
10
+ if (normalized.startsWith('ekk_')) {
11
+ const parts = normalized.split('_');
12
+ if (parts.length >= 2 && parts[1]) {
13
+ return parts[1];
14
+ }
15
+ }
16
+ const colonParts = normalized.split(':');
17
+ if (colonParts.length >= 2 && colonParts[0]) {
18
+ return colonParts[0];
19
+ }
20
+ return undefined;
21
+ }
6
22
  /**
7
23
  * Bind the real session name to the proxy
8
- * This replaces the '_pending' placeholder for this user, enabling proper eviction
24
+ * This replaces the pending placeholder for this user, enabling proper eviction.
9
25
  */
10
- async function bindSession(realSession, projectPath) {
26
+ async function bindSession({ realSession, projectPath, pendingSession, sessionId, }) {
11
27
  try {
12
28
  // Get userId same way as run.ts
13
29
  const config = (0, state_1.getConfig)();
30
+ const authToken = (0, state_1.getAuthToken)();
14
31
  let userId = config?.userId;
15
32
  if (!userId || userId === 'anonymous') {
16
- const token = (0, state_1.getAuthToken)();
17
- if (token?.startsWith('ekk_')) {
18
- const parts = token.split('_');
19
- if (parts.length >= 2) {
20
- userId = parts[1];
21
- }
33
+ if (authToken) {
34
+ userId = extractUserIdFromToken(authToken);
22
35
  }
23
36
  }
24
- if (!userId || userId === 'anonymous')
37
+ if (!userId || userId === 'anonymous' || !authToken)
25
38
  return false;
39
+ const bindAuthToken = authToken.startsWith('ekk_')
40
+ ? authToken
41
+ : `${userId}:${authToken}`;
42
+ const body = {
43
+ userId,
44
+ realSession,
45
+ projectPath,
46
+ };
47
+ if (pendingSession) {
48
+ body.pendingSession = pendingSession;
49
+ }
50
+ if (sessionId) {
51
+ body.sessionId = sessionId;
52
+ }
26
53
  // Use global fetch (Node 18+)
27
54
  const response = await fetch(`${MEMORY_API_URL}/proxy/session/bind`, {
28
55
  method: 'POST',
29
56
  signal: AbortSignal.timeout(2500),
30
57
  headers: {
31
- 'Content-Type': 'application/json'
58
+ 'Content-Type': 'application/json',
59
+ 'x-ekkos-auth-token': bindAuthToken,
60
+ Authorization: `Bearer ${bindAuthToken}`,
32
61
  },
33
- body: JSON.stringify({
34
- userId,
35
- realSession,
36
- projectPath
37
- })
62
+ body: JSON.stringify(body),
38
63
  });
39
64
  return response.ok;
40
65
  }
41
- catch (err) {
66
+ catch {
42
67
  // Fail silently - don't crash CLI
43
68
  return false;
44
69
  }
@@ -80,7 +80,7 @@ export declare function getMostRecentSession(): {
80
80
  */
81
81
  export declare function getActiveSessions(): ActiveSession[];
82
82
  /**
83
- * Register a new active session
83
+ * Register a new active session (for swarm tracking)
84
84
  */
85
85
  export declare function registerActiveSession(sessionId: string, sessionName: string, projectPath: string): ActiveSession;
86
86
  /**
@@ -244,7 +244,7 @@ function getMostRecentSession() {
244
244
  }
245
245
  }
246
246
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
247
- // MULTI-SESSION SUPPORT
247
+ // MULTI-SESSION/SWARM SUPPORT
248
248
  // Track multiple concurrent Claude Code sessions without state collision
249
249
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
250
250
  /**
@@ -271,7 +271,7 @@ function getActiveSessions() {
271
271
  }
272
272
  }
273
273
  /**
274
- * Register a new active session
274
+ * Register a new active session (for swarm tracking)
275
275
  */
276
276
  function registerActiveSession(sessionId, sessionName, projectPath) {
277
277
  ensureEkkosDir();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -364,20 +364,21 @@ Call `ekkOS_Directive` when user says:
364
364
  **EVERY response MUST end with this footer:**
365
365
  ```
366
366
  ---
367
- {IDE} ({Model}) ยท ๐Ÿง  **ekkOS_โ„ข** ยท {SessionName} ยท ๐Ÿ“… {Timestamp}
367
+ {IDE} ({Model}) ยท ๐Ÿง  **ekkOS_โ„ข** ยท Turn {N} ยท {SessionName} ยท ๐Ÿ“… {Timestamp}
368
368
  ```
369
369
 
370
370
  **How to detect values:**
371
371
  - **IDE**: Claude Code, Cursor, Windsurf, etc. (from environment)
372
372
  - **Model**: Sonnet 4.5, Opus 4.5, etc. (from your model name)
373
+ - **Turn Number**: From hook header (e.g., "Turn 47") - starts at 0 for each new session
373
374
  - **Session Name**: From hook header (e.g., "sol-gem-dig") - human-readable session identifier
374
- - **Timestamp**: From hook header (accurate local time in EST, include time)
375
+ - **Timestamp**: From hook header (accurate local time in EST)
375
376
 
376
377
  **Examples:**
377
- - `Claude Code (Opus 4.6) ยท ๐Ÿง  **ekkOS_โ„ข** ยท cosmic-penguin-runs ยท ๐Ÿ“… 2026-01-09 4:50 PM EST`
378
- - `Cursor (Claude Sonnet 4) ยท ๐Ÿง  **ekkOS_โ„ข** ยท bright-falcon-soars ยท ๐Ÿ“… 2026-01-09 10:15 AM EST`
378
+ - `Claude Code (Sonnet 4.5) ยท ๐Ÿง  **ekkOS_โ„ข** ยท Turn 12 ยท cosmic-penguin-runs ยท ๐Ÿ“… 2026-01-09 4:50 PM EST`
379
+ - `Cursor (Claude Sonnet 4) ยท ๐Ÿง  **ekkOS_โ„ข** ยท Turn 5 ยท bright-falcon-soars ยท ๐Ÿ“… 2026-01-09 10:15 AM EST`
379
380
 
380
- **The hook header shows:** `๐Ÿง  ekkOS Memory | {SessionName} | {timestamp}`
381
+ **The hook header shows:** `๐Ÿง  ekkOS Memory | Turn {N} | {Context%} | {SessionName} | {timestamp}`
381
382
 
382
383
  ---
383
384
 
@@ -12,6 +12,14 @@
12
12
 
13
13
  $ErrorActionPreference = "SilentlyContinue"
14
14
 
15
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
16
+ # PROXY MODE DETECTION
17
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
18
+ $ProxyMode = $false
19
+ if ($env:ANTHROPIC_BASE_URL -and $env:ANTHROPIC_BASE_URL -like "*proxy.ekkos.dev*") {
20
+ $ProxyMode = $true
21
+ }
22
+
15
23
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
16
24
  # CONFIG PATHS
17
25
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -87,6 +95,14 @@ function Convert-UuidToWords {
87
95
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
88
96
  $inputJson = [Console]::In.ReadToEnd()
89
97
 
98
+ if ($ProxyMode) {
99
+ $projectState = Join-Path (Join-Path ((Get-Location).Path) ".claude\state") "hook-state.json"
100
+ $globalState = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
101
+ if (Test-Path $projectState) { Remove-Item $projectState -Force }
102
+ if (Test-Path $globalState) { Remove-Item $globalState -Force }
103
+ exit 0
104
+ }
105
+
90
106
  # Get session ID from state
91
107
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
92
108
  $sessionFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
@@ -110,39 +126,37 @@ if ($rawSessionId -eq "unknown" -and (Test-Path $sessionFile)) {
110
126
 
111
127
  $sessionName = Convert-UuidToWords $rawSessionId
112
128
 
129
+ function Get-PendingSessionFromProxyBaseUrl {
130
+ param([string]$baseUrl)
131
+ if (-not $baseUrl) { return $null }
132
+ try {
133
+ $uri = [System.Uri]$baseUrl
134
+ $segments = $uri.AbsolutePath.Trim('/') -split '/'
135
+ if (-not $segments -or $segments.Count -lt 3) { return $null }
136
+ $proxyIndex = [Array]::IndexOf($segments, 'proxy')
137
+ if ($proxyIndex -lt 0 -or $segments.Count -le ($proxyIndex + 2)) { return $null }
138
+ $candidate = [System.Uri]::UnescapeDataString($segments[$proxyIndex + 2])
139
+ if ($candidate -eq '_pending' -or $candidate -eq 'pending' -or $candidate.StartsWith('_pending-')) {
140
+ return $candidate
141
+ }
142
+ } catch {}
143
+ return $null
144
+ }
145
+ # Session binding removed โ€” proxy now self-resolves _pending โ†’ word-based
146
+ # names inline via uuidToSessionName(). No external bind call needed.
147
+
113
148
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
114
- # SESSION BINDING: Bridge _pending โ†’ real session name for proxy eviction
115
- # Windows has no PTY so run.ts can't detect the session name. The stop hook
116
- # is the first place we have a confirmed session name, so we bind here.
117
- # Mac does this in stop.sh (lines 171-179). Logic is identical.
149
+ # AUTH LOADING (direct mode only)
118
150
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
151
+ $authToken = $null
152
+ $userId = $null
119
153
  $configFile = Join-Path $EkkosConfigDir "config.json"
120
- if ((Test-Path $configFile) -and $sessionName -ne "unknown-session") {
154
+ if (Test-Path $configFile) {
121
155
  try {
122
- $config = Get-Content $configFile -Raw | ConvertFrom-Json
123
- $userId = $config.userId
124
- $authToken = if ($config.hookApiKey) { $config.hookApiKey } else { $config.apiKey }
125
-
126
- if ($userId -and $authToken) {
127
- $projectPath = (Get-Location).Path
128
- $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
129
-
130
- $projectPath = $projectPath -replace '\\', '/'
131
- $bindBody = @{
132
- userId = $userId
133
- realSession = $sessionName
134
- projectPath = $projectPath
135
- pendingSession = $pendingSession
136
- } | ConvertTo-Json -Depth 10
137
-
138
- Start-Job -ScriptBlock {
139
- param($body, $token)
140
- Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
141
- -Method POST `
142
- -Headers @{ "Content-Type" = "application/json" } `
143
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
144
- } -ArgumentList $bindBody, $authToken | Out-Null
145
- }
156
+ $cfg = Get-Content $configFile -Raw | ConvertFrom-Json
157
+ $authToken = $cfg.hookApiKey
158
+ if (-not $authToken) { $authToken = $cfg.apiKey }
159
+ $userId = $cfg.userId
146
160
  } catch {}
147
161
  }
148
162
 
@@ -18,6 +18,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
19
19
  STATE_DIR="$PROJECT_ROOT/.claude/state"
20
20
 
21
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
22
+ # PROXY MODE DETECTION
23
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
24
+ PROXY_MODE=false
25
+ if [[ "$ANTHROPIC_BASE_URL" == *"proxy.ekkos.dev"* ]]; then
26
+ PROXY_MODE=true
27
+ fi
28
+
21
29
  mkdir -p "$STATE_DIR" 2>/dev/null
22
30
 
23
31
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -53,6 +61,14 @@ TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
53
61
  MODEL_USED=$(parse_json_value "$INPUT" '.model')
54
62
  [ -z "$MODEL_USED" ] && MODEL_USED="claude-sonnet-4-5"
55
63
 
64
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
65
+ # PROXY MODE: Slim hook path (local cleanup only)
66
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
67
+ if [ "$PROXY_MODE" = "true" ]; then
68
+ rm -f "$STATE_DIR/hook-state.json" "$HOME/.claude/state/hook-state.json" 2>/dev/null || true
69
+ exit 0
70
+ fi
71
+
56
72
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
57
73
  # Session ID
58
74
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -163,20 +179,8 @@ fi
163
179
 
164
180
  MEMORY_API_URL="https://mcp.ekkos.dev"
165
181
 
166
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
167
- # SESSION BINDING: Bridge _pending โ†’ real session name for proxy eviction
168
- # The CLI may be in spawn pass-through mode (no PTY = blind to TUI output),
169
- # so the stop hook (which IS sighted) must bind the session.
170
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
171
- if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session-starts" ] && [ -n "$USER_ID" ]; then
172
- PROJECT_PATH_FOR_BIND=$(pwd)
173
- PENDING_SESSION_FOR_BIND="${EKKOS_PENDING_SESSION:-_pending}"
174
- curl -s -X POST "$MEMORY_API_URL/proxy/session/bind" \
175
- -H "Content-Type: application/json" \
176
- -d "{\"userId\":\"$USER_ID\",\"realSession\":\"$SESSION_NAME\",\"projectPath\":\"$PROJECT_PATH_FOR_BIND\",\"pendingSession\":\"$PENDING_SESSION_FOR_BIND\"}" \
177
- --connect-timeout 1 \
178
- --max-time 2 >/dev/null 2>&1 &
179
- fi
182
+ # Session binding removed โ€” proxy now self-resolves _pending โ†’ word-based
183
+ # names inline via uuidToSessionName(). No external bind call needed.
180
184
 
181
185
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
182
186
  # EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
@@ -12,6 +12,14 @@
12
12
 
13
13
  $ErrorActionPreference = "SilentlyContinue"
14
14
 
15
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
16
+ # PROXY MODE DETECTION
17
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
18
+ $ProxyMode = $false
19
+ if ($env:ANTHROPIC_BASE_URL -and $env:ANTHROPIC_BASE_URL -like "*proxy.ekkos.dev*") {
20
+ $ProxyMode = $true
21
+ }
22
+
15
23
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
16
24
  # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
17
25
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -77,6 +85,100 @@ if ($rawSessionId -eq "unknown") {
77
85
  }
78
86
  }
79
87
 
88
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
89
+ # PROXY MODE: Slim hook path (cosmetic output + local state only)
90
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
91
+ if ($ProxyMode) {
92
+ function Get-ProxySessionName {
93
+ param([string]$sessionId)
94
+ if (-not $sessionId -or $sessionId -eq "unknown") { return "unknown-session" }
95
+ if ($sessionId -notmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
96
+ return $sessionId
97
+ }
98
+
99
+ if (-not $script:SessionWords) { Load-SessionWords }
100
+ if (-not $script:SessionWords) { return $sessionId }
101
+
102
+ $adjectives = $script:SessionWords.adjectives
103
+ $nouns = $script:SessionWords.nouns
104
+ $verbs = $script:SessionWords.verbs
105
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return $sessionId }
106
+
107
+ $clean = $sessionId -replace "-", ""
108
+ if ($clean.Length -lt 12) { return $sessionId }
109
+
110
+ try {
111
+ $adj = [Convert]::ToInt32($clean.Substring(0, 4), 16) % $adjectives.Length
112
+ $noun = [Convert]::ToInt32($clean.Substring(4, 4), 16) % $nouns.Length
113
+ $verb = [Convert]::ToInt32($clean.Substring(8, 4), 16) % $verbs.Length
114
+ return "$($adjectives[$adj])-$($nouns[$noun])-$($verbs[$verb])"
115
+ } catch {
116
+ return $sessionId
117
+ }
118
+ }
119
+
120
+ $sessionName = Get-ProxySessionName $rawSessionId
121
+ $timestampUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
122
+ $displayTime = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt zzz")
123
+ $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
124
+
125
+ New-Item -ItemType Directory -Path $EkkosConfigDir -Force | Out-Null
126
+ New-Item -ItemType Directory -Path (Join-Path $env:USERPROFILE ".claude\state") -Force | Out-Null
127
+
128
+ $globalState = @{
129
+ session_id = $rawSessionId
130
+ session_name = $sessionName
131
+ project = $projectPath
132
+ timestamp = $timestampUtc
133
+ } | ConvertTo-Json -Depth 10 -Compress
134
+ Set-Content -Path (Join-Path $EkkosConfigDir "current-session.json") -Value $globalState -Force
135
+
136
+ $localState = @{
137
+ session_id = $rawSessionId
138
+ session_name = $sessionName
139
+ timestamp = $timestampUtc
140
+ } | ConvertTo-Json -Depth 10 -Compress
141
+ Set-Content -Path (Join-Path $env:USERPROFILE ".claude\state\current-session.json") -Value $localState -Force
142
+
143
+ $activeSessionsPath = Join-Path $EkkosConfigDir "active-sessions.json"
144
+ $sessions = @()
145
+ if (Test-Path $activeSessionsPath) {
146
+ try { $sessions = @(Get-Content $activeSessionsPath -Raw | ConvertFrom-Json) } catch { $sessions = @() }
147
+ }
148
+
149
+ $entry = @{
150
+ sessionId = $rawSessionId
151
+ sessionName = $sessionName
152
+ projectPath = $projectPath
153
+ startedAt = $timestampUtc
154
+ lastHeartbeat = $timestampUtc
155
+ pid = 0
156
+ }
157
+ $idx = -1
158
+ for ($i = 0; $i -lt $sessions.Count; $i++) {
159
+ if ($sessions[$i].sessionId -eq $rawSessionId) { $idx = $i; break }
160
+ }
161
+ if ($idx -ge 0) {
162
+ $entry.startedAt = if ($sessions[$idx].startedAt) { $sessions[$idx].startedAt } else { $timestampUtc }
163
+ $sessions[$idx] = $entry
164
+ } else {
165
+ $sessions += $entry
166
+ }
167
+ Set-Content -Path $activeSessionsPath -Value ($sessions | ConvertTo-Json -Depth 10) -Force
168
+
169
+ $hint = @{
170
+ sessionName = $sessionName
171
+ sessionId = $rawSessionId
172
+ projectPath = $projectPath
173
+ ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
174
+ } | ConvertTo-Json -Depth 10 -Compress
175
+ Set-Content -Path (Join-Path $EkkosConfigDir "hook-session-hint.json") -Value $hint -Force
176
+
177
+ $esc = [char]27
178
+ Write-Output "${esc}[0;36m${esc}[1m๐Ÿง  ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $displayTime${esc}[0m"
179
+ exit 0
180
+ }
181
+
80
182
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
81
183
  # INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
82
184
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -139,39 +241,24 @@ function Convert-UuidToWords {
139
241
 
140
242
  $sessionName = Convert-UuidToWords $rawSessionId
141
243
 
142
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
143
- # PROXY SESSION BIND: _pending โ†’ real session name (fires every turn)
144
- # Mirrors bash user-prompt-submit.sh lines 319-338.
145
- # No PTY on Windows so run.ts can't detect session name โ€” hook must bind it.
146
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
147
- if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
148
- $configFile = Join-Path $EkkosConfigDir "config.json"
149
- if (Test-Path $configFile) {
150
- try {
151
- $config = Get-Content $configFile -Raw | ConvertFrom-Json
152
- $userId = $config.userId
153
- if ($userId) {
154
- $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
155
- $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
156
- $projectPath = $projectPath -replace '\\', '/'
157
- $bindBody = @{
158
- userId = $userId
159
- realSession = $sessionName
160
- projectPath = $projectPath
161
- pendingSession = $pendingSession
162
- } | ConvertTo-Json -Depth 10 -Compress
163
-
164
- Start-Job -ScriptBlock {
165
- param($body)
166
- Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
167
- -Method POST `
168
- -Headers @{ "Content-Type" = "application/json" } `
169
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
170
- } -ArgumentList $bindBody | Out-Null
171
- }
172
- } catch {}
173
- }
244
+ function Get-PendingSessionFromProxyBaseUrl {
245
+ param([string]$baseUrl)
246
+ if (-not $baseUrl) { return $null }
247
+ try {
248
+ $uri = [System.Uri]$baseUrl
249
+ $segments = $uri.AbsolutePath.Trim('/') -split '/'
250
+ if (-not $segments -or $segments.Count -lt 3) { return $null }
251
+ $proxyIndex = [Array]::IndexOf($segments, 'proxy')
252
+ if ($proxyIndex -lt 0 -or $segments.Count -le ($proxyIndex + 2)) { return $null }
253
+ $candidate = [System.Uri]::UnescapeDataString($segments[$proxyIndex + 2])
254
+ if ($candidate -eq '_pending' -or $candidate -eq 'pending' -or $candidate.StartsWith('_pending-')) {
255
+ return $candidate
256
+ }
257
+ } catch {}
258
+ return $null
174
259
  }
260
+ # Session binding removed โ€” proxy now self-resolves _pending โ†’ word-based
261
+ # names inline via uuidToSessionName(). No external bind call needed.
175
262
 
176
263
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
177
264
  # SESSION CURRENT: Update Redis with current session name
@@ -325,7 +412,8 @@ if ($skillReminders.Count -gt 0) {
325
412
 
326
413
  $output += @"
327
414
 
328
- <footer-format>End responses with: Claude Code ({Model}) ยท ๐Ÿง  ekkOS_โ„ข ยท $sessionName ยท ๐Ÿ“… $timestamp</footer-format>
415
+ <footer-format>End responses with: Claude Code ({Model}) ยท ๐Ÿง  ekkOS_โ„ข ยท $sessionName ยท $timestamp</footer-format>
416
+ <footer-note>Do not include a turn counter in the footer.</footer-note>
329
417
  "@
330
418
 
331
419
  Write-Output $output
@@ -13,6 +13,14 @@ set +e
13
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
15
15
 
16
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
17
+ # PROXY MODE DETECTION
18
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
19
+ PROXY_MODE=false
20
+ if [[ "$ANTHROPIC_BASE_URL" == *"proxy.ekkos.dev"* ]]; then
21
+ PROXY_MODE=true
22
+ fi
23
+
16
24
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
17
25
  # CONFIG PATHS - No jq dependency (v1.2 spec)
18
26
  # Session words live in ~/.ekkos/ so they work in ANY project
@@ -64,6 +72,92 @@ if [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ] || [ -z "
64
72
  fi
65
73
  fi
66
74
 
75
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
76
+ # PROXY MODE: Slim hook path (cosmetic output + local state only)
77
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
78
+ if [ "$PROXY_MODE" = "true" ]; then
79
+ SESSION_ID="$RAW_SESSION_ID"
80
+ UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
81
+ SESSION_NAME="$SESSION_ID"
82
+
83
+ if [[ "$SESSION_ID" =~ $UUID_REGEX ]] && [ -f "$JSON_PARSE_HELPER" ]; then
84
+ SESSION_NAME=$(node -e "
85
+ const fs = require('fs');
86
+ const sid = process.argv[1] || '';
87
+ const wordsFile = process.argv[2];
88
+ const fallbackFile = process.argv[3];
89
+ const helper = process.argv[4];
90
+ let wordsPath = wordsFile;
91
+ if (!fs.existsSync(wordsPath)) wordsPath = fallbackFile;
92
+ if (!fs.existsSync(wordsPath) || !fs.existsSync(helper)) {
93
+ console.log(sid || 'unknown-session');
94
+ process.exit(0);
95
+ }
96
+ const cp = require('child_process');
97
+ function readList(path) {
98
+ const out = cp.spawnSync('node', [helper, wordsPath, path], { encoding: 'utf8' });
99
+ if (out.status !== 0) return [];
100
+ return (out.stdout || '').split('\\n').map(s => s.trim()).filter(Boolean);
101
+ }
102
+ const adjectives = readList('.adjectives');
103
+ const nouns = readList('.nouns');
104
+ const verbs = readList('.verbs');
105
+ if (!adjectives.length || !nouns.length || !verbs.length) {
106
+ console.log(sid);
107
+ process.exit(0);
108
+ }
109
+ const hex = sid.replace(/-/g, '').slice(0, 12);
110
+ if (!/^[0-9a-fA-F]{12}$/.test(hex)) {
111
+ console.log(sid);
112
+ process.exit(0);
113
+ }
114
+ const adj = parseInt(hex.slice(0, 4), 16) % adjectives.length;
115
+ const noun = parseInt(hex.slice(4, 8), 16) % nouns.length;
116
+ const verb = parseInt(hex.slice(8, 12), 16) % verbs.length;
117
+ console.log(adjectives[adj] + '-' + nouns[noun] + '-' + verbs[verb]);
118
+ " "$SESSION_ID" "$SESSION_WORDS_JSON" "$SESSION_WORDS_DEFAULT" "$JSON_PARSE_HELPER" 2>/dev/null || echo "$SESSION_ID")
119
+ fi
120
+
121
+ TIMESTAMP_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)
122
+ DISPLAY_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
123
+ mkdir -p "$HOME/.ekkos" "$HOME/.claude/state" 2>/dev/null || true
124
+ echo "{\"session_id\":\"$SESSION_ID\",\"session_name\":\"$SESSION_NAME\",\"project\":\"$PROJECT_ROOT\",\"timestamp\":\"$TIMESTAMP_UTC\"}" > "$HOME/.ekkos/current-session.json" 2>/dev/null || true
125
+ echo "{\"session_id\":\"$SESSION_ID\",\"session_name\":\"$SESSION_NAME\",\"timestamp\":\"$TIMESTAMP_UTC\"}" > "$HOME/.claude/state/current-session.json" 2>/dev/null || true
126
+
127
+ ACTIVE_SESSIONS_FILE="$HOME/.ekkos/active-sessions.json"
128
+ node -e "
129
+ const fs = require('fs');
130
+ const file = process.argv[1];
131
+ const sid = process.argv[2];
132
+ const sname = process.argv[3];
133
+ const ts = process.argv[4];
134
+ const project = process.argv[5];
135
+ let sessions = [];
136
+ try {
137
+ if (fs.existsSync(file)) {
138
+ sessions = JSON.parse(fs.readFileSync(file, 'utf8') || '[]');
139
+ }
140
+ } catch {}
141
+ const index = sessions.findIndex(s => s.sessionId === sid);
142
+ if (index >= 0) {
143
+ sessions[index] = { ...sessions[index], sessionName: sname, projectPath: project, lastHeartbeat: ts };
144
+ } else {
145
+ sessions.push({ sessionId: sid, sessionName: sname, projectPath: project, startedAt: ts, lastHeartbeat: ts, pid: 0 });
146
+ }
147
+ fs.writeFileSync(file, JSON.stringify(sessions, null, 2));
148
+ " "$ACTIVE_SESSIONS_FILE" "$SESSION_ID" "$SESSION_NAME" "$TIMESTAMP_UTC" "$PROJECT_ROOT" 2>/dev/null || true
149
+
150
+ # Dashboard/local tooling hint file
151
+ echo "{\"sessionName\":\"$SESSION_NAME\",\"sessionId\":\"$SESSION_ID\",\"projectPath\":\"$PROJECT_ROOT\",\"ts\":$(date +%s)000}" > "$HOME/.ekkos/hook-session-hint.json" 2>/dev/null || true
152
+
153
+ CYAN='\033[0;36m'
154
+ DIM='\033[2m'
155
+ BOLD='\033[1m'
156
+ RESET='\033[0m'
157
+ echo -e "${CYAN}${BOLD}๐Ÿง  ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${DISPLAY_TIME}${RESET}"
158
+ exit 0
159
+ fi
160
+
67
161
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
68
162
  # SKILL AUTO-FIRE: Detect keywords and inject skill reminders
69
163
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -138,7 +232,6 @@ echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:
138
232
  PROJECT_SESSION_DIR="$STATE_DIR/sessions"
139
233
  mkdir -p "$PROJECT_SESSION_DIR" 2>/dev/null || true
140
234
  TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
141
- CONTEXT_SIZE_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.context"
142
235
 
143
236
  # Count API round-trips from transcript to match TUI turn counter
144
237
  TURN_NUMBER=1
@@ -159,28 +252,7 @@ fi
159
252
 
160
253
  echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
161
254
 
162
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
163
- # Context size tracking - Uses tokenizer script (single source)
164
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
165
- PREV_CONTEXT_PERCENT=0
166
- [ -f "$CONTEXT_SIZE_FILE" ] && PREV_CONTEXT_PERCENT=$(cat "$CONTEXT_SIZE_FILE" 2>/dev/null || echo "0")
167
-
168
- TOKEN_PERCENT=0
169
- MAX_TOKENS=200000
170
- TOKENIZER_SCRIPT="$SCRIPT_DIR/lib/count-tokens.cjs"
171
-
172
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -f "$TOKENIZER_SCRIPT" ]; then
173
- TOKEN_COUNT=$(node "$TOKENIZER_SCRIPT" "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
174
- if [[ "$TOKEN_COUNT" =~ ^[0-9]+$ ]] && [ "$TOKEN_COUNT" -gt 0 ]; then
175
- TOKEN_PERCENT=$((TOKEN_COUNT * 100 / MAX_TOKENS))
176
- [ "$TOKEN_PERCENT" -gt 100 ] && TOKEN_PERCENT=100
177
- # In proxy mode, IPC compresses ~65-70% โ€” show estimated post-compression %
178
- IPC_PERCENT=$((TOKEN_PERCENT * 30 / 100))
179
- [ "$IPC_PERCENT" -lt 1 ] && IPC_PERCENT=1
180
- fi
181
- fi
182
255
 
183
- echo "$TOKEN_PERCENT" > "$CONTEXT_SIZE_FILE"
184
256
 
185
257
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
186
258
  # COLORS
@@ -311,32 +383,8 @@ try {
311
383
  --max-time 3 >/dev/null 2>&1 &
312
384
  fi
313
385
 
314
- # 6. CRITICAL: Bind session name to proxy for R2 eviction paths - No jq
315
- EKKOS_USER_ID=""
316
- if [ -f "$HOME/.ekkos/config.json" ] && [ -f "$JSON_PARSE_HELPER" ]; then
317
- EKKOS_USER_ID=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.userId' 2>/dev/null || echo "")
318
- fi
319
- if [ -n "$EKKOS_USER_ID" ] && [ -n "$SESSION_NAME" ]; then
320
- PENDING_SESSION="${EKKOS_PENDING_SESSION:-_pending}"
321
- node -e "
322
- const https = require('https');
323
- const body = JSON.stringify({
324
- userId: process.argv[1],
325
- realSession: process.argv[2],
326
- projectPath: process.argv[3],
327
- pendingSession: process.argv[4]
328
- });
329
- const req = https.request({
330
- hostname: 'mcp.ekkos.dev',
331
- path: '/proxy/session/bind',
332
- method: 'POST',
333
- headers: {'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body)}
334
- }, () => {});
335
- req.on('error', () => {});
336
- req.write(body);
337
- req.end();
338
- " "$EKKOS_USER_ID" "$SESSION_NAME" "$PROJECT_ROOT" "$PENDING_SESSION" 2>/dev/null &
339
- fi
386
+ # Session binding removed โ€” proxy now self-resolves _pending โ†’ word-based
387
+ # names inline via uuidToSessionName(). No external bind call needed.
340
388
  fi
341
389
 
342
390
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -410,49 +458,7 @@ turns.slice(0, -1).forEach(t => {
410
458
  fi
411
459
  fi
412
460
 
413
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
414
- # COMPACTION DETECTION: If context dropped dramatically, auto-restore
415
- # Was >50% last turn, now <15% = compaction happened
416
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
417
- if [ "$PREV_CONTEXT_PERCENT" -gt 50 ] && [ "$TOKEN_PERCENT" -lt 15 ] && [ -n "$AUTH_TOKEN" ]; then
418
- echo ""
419
- echo -e "${GREEN}${BOLD}๐Ÿ”„ CONTEXT RESTORED${RESET} ${DIM}| Compaction detected | Auto-loading recent turns...${RESET}"
420
-
421
- RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
422
- -H "Authorization: Bearer $AUTH_TOKEN" \
423
- -H "Content-Type: application/json" \
424
- -d "{\"session_id\": \"${SESSION_ID}\", \"last_n\": 10, \"format\": \"summary\"}" \
425
- --connect-timeout 3 \
426
- --max-time 5 2>/dev/null || echo '{"turns":[]}')
427
-
428
- RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
429
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
430
- console.log((d.turns || []).length);
431
- " 2>/dev/null || echo "0")
432
-
433
- if [ "$RESTORED_COUNT" -gt 0 ]; then
434
- echo -e "${GREEN} โœ“${RESET} Restored ${RESTORED_COUNT} turns from Layer 2"
435
- echo ""
436
- echo -e "${MAGENTA}${BOLD}## Recent Context (auto-restored)${RESET}"
437
- echo ""
438
-
439
- echo "$RESTORE_RESPONSE" | node -e "
440
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
441
- (d.turns || []).forEach(t => {
442
- const q = (t.user_query || '...').substring(0, 120);
443
- const a = (t.assistant_response || '...').substring(0, 250);
444
- console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...\n> ' + a + '...\n');
445
- });
446
- " 2>/dev/null || true
447
-
448
- echo ""
449
- echo -e "${DIM}Full history: \"turns 1-${TURN_NUMBER}\" or \"recall yesterday\"${RESET}"
450
- fi
451
-
452
- echo ""
453
- echo -e "${CYAN}${BOLD}๐Ÿง  ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
454
-
455
- elif [ "$POST_CLEAR_DETECTED" = true ] && [ -n "$AUTH_TOKEN" ]; then
461
+ if [ "$POST_CLEAR_DETECTED" = true ] && [ -n "$AUTH_TOKEN" ]; then
456
462
  # /clear detected - show visible restoration banner
457
463
  echo -e "${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${RESET}" >&2
458
464
  echo -e "${GREEN}${BOLD}๐Ÿ”„ SESSION CONTINUED${RESET} ${DIM}| ${TURN_NUMBER} turns preserved | Context restored${RESET}" >&2
@@ -497,9 +503,6 @@ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
497
503
  echo ""
498
504
  echo -e "${CYAN}${BOLD}๐Ÿง  ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
499
505
 
500
- elif [ "$TOKEN_PERCENT" -ge 50 ]; then
501
- echo -e "${CYAN}${BOLD}๐Ÿง  ekkOS Memory${RESET} ${DIM}| ~${IPC_PERCENT:-0}% IPC | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
502
-
503
506
  else
504
507
  echo -e "${CYAN}${BOLD}๐Ÿง  ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
505
508
  fi