@retaw106/mmc-pi 1.2.3 → 2.0.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.
package/dist/web/main.js CHANGED
@@ -426,6 +426,9 @@ ${data.stack}` : ""}`)}` : ` ${JSON.stringify(sanitizeConfigValue(data))}`;
426
426
  }
427
427
  } catch {}
428
428
  }
429
+ function sessionLog(sessionId, message, data) {
430
+ log(`[magic-context][${sessionId}] ${message}`, data);
431
+ }
429
432
  var isTestEnv = false, buffer, flushTimer = null, FLUSH_INTERVAL_MS = 500, BUFFER_SIZE_LIMIT = 50, MAX_BUFFERED_BYTES, bufferedBytes = 0, droppedLines = 0, MAX_LOG_FILE_BYTES, SIZE_CHECK_INTERVAL_FLUSHES = 64, activeLogFile = null, activeLogSize = null, flushesSinceSizeCheck = 0, swallowedWriteCount = 0, lastErrorMessage = null, lastErrorTime = null;
430
433
  var init_logger = __esm(() => {
431
434
  init_data_path();
@@ -822,12 +825,13 @@ function isKnownAutocommit(db) {
822
825
  }
823
826
 
824
827
  // ../plugin/src/shared/write-transaction-timing.ts
825
- function logSlowWriteTransaction(site, startedAt, thresholdMs = SLOW_WRITE_TRANSACTION_THRESHOLD_MS, completedAtMs = performance.now()) {
828
+ function logSlowWriteTransaction(site, startedAt, thresholdMs = SLOW_WRITE_TRANSACTION_THRESHOLD_MS, completedAtMs = performance.now(), steps) {
826
829
  try {
827
830
  const durationMs = completedAtMs - startedAt;
828
831
  if (durationMs < thresholdMs)
829
832
  return;
830
- log(`[magic-context] slow write transaction: site=${site} held=${durationMs.toFixed(1)}ms`);
833
+ const detail = steps ? Object.entries(steps).map(([name, ms]) => `${name}=${ms.toFixed(1)}ms`).join(" ") : "";
834
+ log(`[magic-context] slow write transaction: site=${site} held=${durationMs.toFixed(1)}ms${detail ? ` ${detail}` : ""}`);
831
835
  } catch {}
832
836
  }
833
837
  var SLOW_WRITE_TRANSACTION_THRESHOLD_MS = 1000;
@@ -835,6 +839,35 @@ var init_write_transaction_timing = __esm(() => {
835
839
  init_logger();
836
840
  });
837
841
 
842
+ // ../plugin/src/features/magic-context/migration-v3-perf-schema.ts
843
+ function installV3PerfSchema(db) {
844
+ const schema = new Map(db.prepare("SELECT name, sql FROM sqlite_master").all().map((row) => [row.name, row.sql]));
845
+ for (const [table, name, columns] of indexes) {
846
+ if (schema.has(table) && !schema.has(name))
847
+ db.exec(`CREATE INDEX ${name} ON ${table}(${columns})`);
848
+ }
849
+ for (const name of V3_REDUNDANT_INDEXES) {
850
+ if (schema.has(name))
851
+ db.exec(`DROP INDEX ${name}`);
852
+ }
853
+ }
854
+ var V3_REDUNDANT_INDEXES, indexes, tagOrderIndexes;
855
+ var init_migration_v3_perf_schema = __esm(() => {
856
+ V3_REDUNDANT_INDEXES = [
857
+ "idx_tags_session_tag_number",
858
+ "idx_compartments_session",
859
+ "idx_pending_ops_session",
860
+ "idx_source_contents_session",
861
+ "idx_compression_depth_session",
862
+ "idx_transform_decisions_session_harness",
863
+ "idx_project_key_files_project"
864
+ ];
865
+ indexes = [
866
+ ["message_fts_rowid_map", "idx_message_fts_rowid_map_session_rowid", "session_id, fts_rowid"]
867
+ ];
868
+ tagOrderIndexes = new WeakMap;
869
+ });
870
+
838
871
  // ../plugin/src/shared/commit-detection.ts
839
872
  var HASH_HEX = "[0-9a-f]{7,12}", COMMIT_HASH_TEST_PATTERN;
840
873
  var init_commit_detection = __esm(() => {
@@ -1010,6 +1043,17 @@ var init_opencode_db_path = __esm(() => {
1010
1043
  claimedDiagnostics = new Set;
1011
1044
  });
1012
1045
 
1046
+ // ../plugin/src/hooks/magic-context/host-served-rows.ts
1047
+ function markHostUnservedRow(target) {
1048
+ Object.defineProperty(target, HOST_UNSERVED_ROW, {
1049
+ value: true,
1050
+ enumerable: false,
1051
+ configurable: true
1052
+ });
1053
+ return target;
1054
+ }
1055
+ var HOST_UNSERVED_ROW = "hostUnservedRow";
1056
+
1013
1057
  // ../plugin/src/features/magic-context/storage-db.ts
1014
1058
  import { basename, dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
1015
1059
  init_data_path();
@@ -1249,9 +1293,69 @@ function splitReplayDecisions(db) {
1249
1293
  db.exec("DROP TABLE temp.v2_replay_documents");
1250
1294
  }
1251
1295
 
1296
+ // ../plugin/src/features/magic-context/storage-db.ts
1297
+ init_migration_v3_perf_schema();
1298
+
1299
+ // ../plugin/src/features/magic-context/migration-v4-temporal-decisions.ts
1300
+ init_logger();
1301
+
1302
+ // ../plugin/src/features/magic-context/temporal-decisions.ts
1303
+ var PREFIX = "temporal-message-v1:";
1304
+ function decodeTemporalDecision(entry) {
1305
+ if (!entry.startsWith(PREFIX))
1306
+ return null;
1307
+ try {
1308
+ const pair = JSON.parse(entry.slice(PREFIX.length));
1309
+ if (Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && pair[0] && typeof pair[1] === "string")
1310
+ return [pair[0], pair[1]];
1311
+ } catch {}
1312
+ return null;
1313
+ }
1314
+
1315
+ // ../plugin/src/features/magic-context/migration-v4-temporal-decisions.ts
1316
+ var TEMPORAL_DECISIONS_DDL = `CREATE TABLE IF NOT EXISTS temporal_decisions (
1317
+ session_id TEXT NOT NULL,
1318
+ message_id TEXT NOT NULL,
1319
+ marker TEXT,
1320
+ PRIMARY KEY (session_id, message_id)
1321
+ ) WITHOUT ROWID`;
1322
+ function installV4TemporalDecisions(db) {
1323
+ const schema = new Map(db.prepare("SELECT name FROM sqlite_master").all().map((row) => [row.name, true]));
1324
+ if (!schema.has("temporal_decisions")) {
1325
+ db.exec(TEMPORAL_DECISIONS_DDL);
1326
+ if (schema.has("session_meta")) {
1327
+ const rows = db.prepare("SELECT session_id, merged_reasoning_stripped_ids AS entries FROM session_meta WHERE merged_reasoning_stripped_ids LIKE '%temporal-message-v1:%'").all();
1328
+ const insert = db.prepare("INSERT OR IGNORE INTO temporal_decisions(session_id,message_id,marker) VALUES (?,?,?)");
1329
+ for (const row of rows) {
1330
+ let entries;
1331
+ try {
1332
+ entries = JSON.parse(row.entries);
1333
+ } catch {
1334
+ sessionLog(row.session_id, "v4 temporal metadata: malformed JSON skipped; original blob retained");
1335
+ continue;
1336
+ }
1337
+ if (!Array.isArray(entries)) {
1338
+ sessionLog(row.session_id, "v4 temporal metadata: non-array JSON skipped; original blob retained");
1339
+ continue;
1340
+ }
1341
+ const kept = [];
1342
+ for (const entry of entries) {
1343
+ const decision = typeof entry === "string" ? decodeTemporalDecision(entry) : null;
1344
+ if (decision)
1345
+ insert.run(row.session_id, ...decision);
1346
+ else
1347
+ kept.push(entry);
1348
+ }
1349
+ db.prepare("UPDATE session_meta SET merged_reasoning_stripped_ids=? WHERE session_id=?").run(JSON.stringify(kept), row.session_id);
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+
1252
1355
  // ../plugin/src/features/magic-context/migrations.ts
1253
1356
  init_logger();
1254
1357
  init_write_transaction_timing();
1358
+ init_migration_v3_perf_schema();
1255
1359
  var FORK_MIGRATION_VERSION_FLOOR = 1e4;
1256
1360
  class MigrationLockBusyError extends Error {
1257
1361
  constructor(message) {
@@ -1280,6 +1384,20 @@ var MIGRATIONS = [
1280
1384
  splitLkgSlotPrefixes(db);
1281
1385
  splitReplayDecisions(db);
1282
1386
  }
1387
+ },
1388
+ {
1389
+ version: 3,
1390
+ description: "drop redundant single-column session indexes; add FTS rowid-map lookup",
1391
+ up(db) {
1392
+ installV3PerfSchema(db);
1393
+ }
1394
+ },
1395
+ {
1396
+ version: 4,
1397
+ description: "store temporal replay decisions per message identity",
1398
+ up(db) {
1399
+ installV4TemporalDecisions(db);
1400
+ }
1283
1401
  }
1284
1402
  ];
1285
1403
  var LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max, m) => Math.max(max, m.version), 0);
@@ -1437,7 +1555,7 @@ var pendingAsyncOpens = new Map;
1437
1555
  var persistenceByDatabase = new WeakMap;
1438
1556
  var persistenceErrorByDatabase = new WeakMap;
1439
1557
  var pathByDatabase = new WeakMap;
1440
- var LATEST_SUPPORTED_VERSION = 2;
1558
+ var LATEST_SUPPORTED_VERSION = 4;
1441
1559
  var BOOT_SQLITE_BUSY_TIMEOUT_MS = 5000;
1442
1560
  function resolveBootBusyTimeoutMs(value) {
1443
1561
  if (value === undefined)
@@ -1534,7 +1652,6 @@ function initializeDatabase(db, busyTimeoutMs = BOOT_SQLITE_BUSY_TIMEOUT_MS) {
1534
1652
  harness TEXT NOT NULL DEFAULT 'opencode',
1535
1653
  UNIQUE(session_id, sequence)
1536
1654
  );
1537
- CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
1538
1655
 
1539
1656
  CREATE TABLE IF NOT EXISTS compartment_chunk_embeddings (
1540
1657
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1595,7 +1712,6 @@ function initializeDatabase(db, busyTimeoutMs = BOOT_SQLITE_BUSY_TIMEOUT_MS) {
1595
1712
  harness TEXT NOT NULL DEFAULT 'opencode',
1596
1713
  PRIMARY KEY(session_id, message_ordinal)
1597
1714
  );
1598
- CREATE INDEX IF NOT EXISTS idx_compression_depth_session ON compression_depth(session_id);
1599
1715
 
1600
1716
  CREATE TABLE IF NOT EXISTS session_facts (
1601
1717
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -2114,7 +2230,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
2114
2230
  stale_reason TEXT,
2115
2231
  PRIMARY KEY (project_path, path)
2116
2232
  );
2117
- CREATE INDEX IF NOT EXISTS idx_project_key_files_project ON project_key_files(project_path);
2118
2233
  CREATE INDEX IF NOT EXISTS idx_project_key_files_generated_at ON project_key_files(project_path, generated_at);
2119
2234
 
2120
2235
  CREATE TABLE IF NOT EXISTS project_key_files_version (
@@ -2474,10 +2589,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
2474
2589
  input_tokens INTEGER NOT NULL DEFAULT 0,
2475
2590
  PRIMARY KEY (session_id, harness, message_id)
2476
2591
  );
2477
- CREATE INDEX IF NOT EXISTS idx_transform_decisions_session_harness
2478
- ON transform_decisions(session_id, harness);
2479
-
2480
- CREATE INDEX IF NOT EXISTS idx_tags_session_tag_number ON tags(session_id, tag_number);
2481
2592
  CREATE INDEX IF NOT EXISTS idx_tags_session_message_id ON tags(session_id, message_id);
2482
2593
 
2483
2594
  -- Clone/import paths can write tags before session bootstrap. Keep trigger-created
@@ -2523,10 +2634,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
2523
2634
  WHERE NEW.session_id != OLD.session_id
2524
2635
  ON CONFLICT(session_id) DO UPDATE SET tags_version = tags_version + 1;
2525
2636
  END;
2526
- CREATE INDEX IF NOT EXISTS idx_pending_ops_session ON pending_ops(session_id);
2527
2637
  CREATE INDEX IF NOT EXISTS idx_pending_ops_session_tag_id ON pending_ops(session_id, tag_id);
2528
- CREATE INDEX IF NOT EXISTS idx_source_contents_session ON source_contents(session_id);
2529
-
2530
2638
  CREATE TABLE IF NOT EXISTS recomp_compartments (
2531
2639
  id INTEGER PRIMARY KEY AUTOINCREMENT,
2532
2640
  session_id TEXT NOT NULL,
@@ -2871,8 +2979,6 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
2871
2979
  input_tokens INTEGER NOT NULL DEFAULT 0,
2872
2980
  PRIMARY KEY (session_id, harness, message_id)
2873
2981
  );
2874
- CREATE INDEX IF NOT EXISTS idx_transform_decisions_session_harness
2875
- ON transform_decisions(session_id, harness);
2876
2982
  `);
2877
2983
  ensureColumn(db, "transform_decisions", "system_hash_prev", "TEXT");
2878
2984
  ensureColumn(db, "transform_decisions", "system_hash_new", "TEXT");
@@ -2908,6 +3014,8 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
2908
3014
  ON tags(session_id, message_id)
2909
3015
  WHERE type = 'tool' AND tool_owner_message_id IS NULL;
2910
3016
  `);
3017
+ installV3PerfSchema(db);
3018
+ installV4TemporalDecisions(db);
2911
3019
  }
2912
3020
  var CHANNEL2_CLAIM_TTL_MS = 10 * 60000;
2913
3021
  var channel2HealStatements = new WeakMap;
@@ -2999,6 +3107,20 @@ function listWebProjects(store) {
2999
3107
  };
3000
3108
  });
3001
3109
  }
3110
+ function listProjectSessionRefs(store) {
3111
+ return store.db.prepare(`SELECT project_path, session_id, harness, MAX(updated_at) AS last_activity
3112
+ FROM session_projects
3113
+ GROUP BY project_path, session_id, harness
3114
+ ORDER BY project_path ASC, session_id ASC, harness ASC`).all().map((row) => {
3115
+ const r = row;
3116
+ return {
3117
+ projectPath: r.project_path,
3118
+ sessionId: r.session_id,
3119
+ harness: r.harness,
3120
+ lastActivityAt: typeof r.last_activity === "number" ? r.last_activity : null
3121
+ };
3122
+ });
3123
+ }
3002
3124
  function listProjectSessions(store, projectPath) {
3003
3125
  return store.db.prepare(`SELECT sp.session_id AS session_id,
3004
3126
  sp.harness AS harness,
@@ -3032,6 +3154,76 @@ function closeQuietly2(db) {
3032
3154
  } catch {}
3033
3155
  }
3034
3156
 
3157
+ // ../plugin/src/web/entry-args.ts
3158
+ function webEntryUsage() {
3159
+ return `用法:mmc web [参数]
3160
+ ` + `
3161
+ ` + `参数:
3162
+ ` + ` --port <整数> 监听端口(0..65535,0 表示随机端口);默认 4180,
3163
+ ` + ` 也可用环境变量 MMC_WEB_PORT 指定(非法值静默忽略)
3164
+ ` + ` --smoke 打印数据探针 JSON 后立即退出,不起服务
3165
+ ` + ` --fixture 用隔离的合成演示数据启动(不读真实数据)
3166
+ ` + ` --fixture-root <目录> 指定演示数据根目录(幂等,可跨重启复用)
3167
+ ` + ` -h, --help 打印本帮助并退出(不读库、不监听)
3168
+ ` + `
3169
+ ` + `未知参数、缺值或非法值一律拒绝(退出码 2);运行期失败退出码 1。
3170
+ `;
3171
+ }
3172
+ function parseWebEntryArgs(args, env = process.env) {
3173
+ let port;
3174
+ let fixtureRoot;
3175
+ let fixture = false;
3176
+ let smoke = false;
3177
+ for (let i = 0;i < args.length; i++) {
3178
+ const token = args[i];
3179
+ if (token === "-h" || token === "--help") {
3180
+ return { kind: "help" };
3181
+ }
3182
+ if (token === "--smoke") {
3183
+ smoke = true;
3184
+ continue;
3185
+ }
3186
+ if (token === "--fixture") {
3187
+ fixture = true;
3188
+ continue;
3189
+ }
3190
+ if (token === "--port" || token === "--fixture-root") {
3191
+ const value = args[i + 1];
3192
+ if (value === undefined || value.startsWith("-")) {
3193
+ return { kind: "usage-error", message: `参数 ${token} 缺少值。` };
3194
+ }
3195
+ if (token === "--port") {
3196
+ const parsed = Number(value);
3197
+ if (value === "" || !Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65535) {
3198
+ return {
3199
+ kind: "usage-error",
3200
+ message: `参数 --port 的值非法:${value}(需要 0..65535 的整数)。`
3201
+ };
3202
+ }
3203
+ port = parsed;
3204
+ } else {
3205
+ fixtureRoot = value;
3206
+ }
3207
+ i++;
3208
+ continue;
3209
+ }
3210
+ return { kind: "usage-error", message: `未知参数:${token}` };
3211
+ }
3212
+ if (port === undefined && env.MMC_WEB_PORT) {
3213
+ const fromEnv = Number(env.MMC_WEB_PORT);
3214
+ if (Number.isSafeInteger(fromEnv) && fromEnv >= 0 && fromEnv <= 65535) {
3215
+ port = fromEnv;
3216
+ }
3217
+ }
3218
+ return {
3219
+ kind: "ok",
3220
+ mode: smoke ? "smoke" : "serve",
3221
+ port,
3222
+ fixture,
3223
+ fixtureRoot
3224
+ };
3225
+ }
3226
+
3035
3227
  // ../plugin/src/web/fixtures.ts
3036
3228
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, writeFileSync } from "node:fs";
3037
3229
  import { tmpdir as tmpdir2 } from "node:os";
@@ -3195,24 +3387,26 @@ function seedOc2Store(dbPath, generation) {
3195
3387
  session.run(FIXTURE_SEED.oc2Historian, "/fixture/project", "Magic Context historian", null, null, T0 + 8000, T0 + 8500);
3196
3388
  session.run(FIXTURE_SEED.oc2HostOnly, "/fixture/project", "fixture: 未注册的宿主会话", null, null, T0, T0 + 1000);
3197
3389
  const message = db.prepare("INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?, ?)");
3198
- const part = (id, type, text) => ({ id, type, text });
3199
3390
  message.run("msg_fixture_idle", FIXTURE_SEED.oc2Session, "idle", 0, T0, T0, JSON.stringify({ outcome: "succeeded" }));
3200
- message.run(OC2_LINKED_START, FIXTURE_SEED.oc2Session, "user", 1, T0 + 100, T0 + 100, JSON.stringify({
3201
- parts: [part("p1", "text", "fixture: 审阅 web 面板的数据层,先看项目列表。")]
3202
- }));
3203
- message.run(OC2_LINKED_END, FIXTURE_SEED.oc2Session, "assistant", 2, T0 + 300, T0 + 300, JSON.stringify({
3204
- parts: [
3205
- part("p3", "text", "fixture: 项目枚举与池语义已核对,继续会话列表。"),
3391
+ message.run(OC2_LINKED_START, FIXTURE_SEED.oc2Session, "user", 1, T0 + 100, T0 + 100, JSON.stringify({ text: "fixture: 审阅 web 面板的数据层,先看项目列表。" }));
3392
+ message.run("msg_fixture_switch", FIXTURE_SEED.oc2Session, "agent-switched", 2, T0 + 200, T0 + 200, JSON.stringify({ agent: "fixer" }));
3393
+ message.run(OC2_LINKED_END, FIXTURE_SEED.oc2Session, "assistant", 3, T0 + 300, T0 + 300, JSON.stringify({
3394
+ content: [
3395
+ { type: "text", text: "fixture: 项目枚举与池语义已核对,继续会话列表。" },
3396
+ { type: "reasoning", text: "fixture: 先确认分页锚点,再看工具输出。" },
3206
3397
  {
3207
- id: "pt",
3208
3398
  type: "tool",
3209
- callID: "call-1",
3210
- tool: "read",
3211
- state: { output: LONG_OUTPUT }
3399
+ name: "read",
3400
+ id: "call-1",
3401
+ state: {
3402
+ status: "completed",
3403
+ input: { path: "views.ts" },
3404
+ content: [{ type: "text", text: LONG_OUTPUT }]
3405
+ }
3212
3406
  }
3213
3407
  ]
3214
3408
  }));
3215
- message.run("msg_hist_1", FIXTURE_SEED.oc2Historian, "user", 1, T0 + 8100, T0 + 8100, JSON.stringify({ parts: [part("ph", "text", "fixture: historian chunk prompt")] }));
3409
+ message.run("msg_hist_1", FIXTURE_SEED.oc2Historian, "user", 1, T0 + 8100, T0 + 8100, JSON.stringify({ text: "fixture: historian chunk prompt" }));
3216
3410
  } finally {
3217
3411
  db.close();
3218
3412
  }
@@ -3343,6 +3537,7 @@ function readPiSessionHeader(filePath, fileName, directoryName) {
3343
3537
  directoryName,
3344
3538
  sessionId: null,
3345
3539
  headerTimestamp: null,
3540
+ headerCwd: null,
3346
3541
  headerError: null
3347
3542
  };
3348
3543
  let firstLine;
@@ -3386,7 +3581,8 @@ function readPiSessionHeader(filePath, fileName, directoryName) {
3386
3581
  return {
3387
3582
  ...base,
3388
3583
  sessionId: record.id,
3389
- headerTimestamp: typeof record.timestamp === "string" ? record.timestamp : null
3584
+ headerTimestamp: typeof record.timestamp === "string" ? record.timestamp : null,
3585
+ headerCwd: typeof record.cwd === "string" && record.cwd.length > 0 ? record.cwd : null
3390
3586
  };
3391
3587
  }
3392
3588
  function readPiSessionFileEntries(filePath) {
@@ -3533,85 +3729,18 @@ import { createHash as createHash4, randomUUID, timingSafeEqual } from "node:cry
3533
3729
  import { createReadStream, existsSync as existsSync8, statSync as statSync3 } from "node:fs";
3534
3730
  import { createServer } from "node:http";
3535
3731
  import { extname, normalize, resolve as resolve4 } from "node:path";
3536
- import { fileURLToPath } from "node:url";
3732
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3537
3733
 
3538
3734
  // ../plugin/src/web/api.ts
3539
3735
  import { existsSync as existsSync6 } from "node:fs";
3540
3736
 
3541
- // ../plugin/src/hooks/magic-context/decay-curve.ts
3542
- var H50 = 24;
3543
- var D = 25;
3544
- var G = 2;
3545
- var Z1 = 0.201;
3546
- var Z4 = 2.587;
3547
- var P_FLOOR = 0.1;
3548
- var TIER_COST = [0, 322, 109, 35, 20, 5];
3549
- function tier(compartmentIndex, importance, budgetPressure) {
3550
- const a = Math.max(compartmentIndex, 1) - 1;
3551
- const imp = Math.max(1, Math.min(100, importance));
3552
- const p = Math.max(budgetPressure, P_FLOOR);
3553
- const F = 2 ** ((imp - 50) / D);
3554
- const H = H50 * F / p;
3555
- const z = a / H;
3556
- if (z < Z1)
3557
- return 1;
3558
- if (z < Z4)
3559
- return 4;
3560
- return 5;
3561
- }
3562
- function shouldArchive(compartmentIndex, importance, budgetPressure, anchorOverlap = 0) {
3563
- const a = Math.max(compartmentIndex, 1) - 1;
3564
- const imp = Math.max(1, Math.min(100, importance));
3565
- const p = Math.max(budgetPressure, P_FLOOR);
3566
- const o = Math.max(0, Math.min(1, anchorOverlap));
3567
- const F = 2 ** ((imp - 50) / D);
3568
- const H = H50 * F / p;
3569
- const z = a / H;
3570
- return z >= Z4 + G * o;
3571
- }
3572
- function renderedTier(compartmentIndex, importance, budgetPressure, anchorOverlap = 0) {
3573
- if (shouldArchive(compartmentIndex, importance, budgetPressure, anchorOverlap)) {
3574
- return 5;
3575
- }
3576
- const base = tier(compartmentIndex, importance, budgetPressure);
3577
- return Math.min(base, 4);
3578
- }
3579
- function computeBudgetPressure(compartments, historyBudget) {
3580
- if (historyBudget <= 0)
3581
- return 1;
3582
- let naturalCost = 0;
3583
- for (const c of compartments) {
3584
- const naturalTier = tier(c.index, c.importance, 1);
3585
- naturalCost += naturalTier >= 5 ? 0 : TIER_COST[naturalTier];
3586
- }
3587
- return Math.max(P_FLOOR, naturalCost / historyBudget);
3588
- }
3737
+ // ../plugin/src/web/display-path.ts
3738
+ import { isAbsolute as isAbsolute3 } from "node:path";
3589
3739
 
3590
- // ../plugin/src/hooks/magic-context/decay-render.ts
3591
- init_read_session_formatting();
3592
- var DEFAULT_HISTORY_BUDGET_TOKENS = 60000;
3593
- function legacyTier(c) {
3594
- return /^U:/m.test(c.content) ? 3 : 4;
3595
- }
3596
- function computeTiers(compartments, historyBudgetTokens) {
3597
- const v2Compartments = compartments.map((c, originalIndex) => ({ c, originalIndex })).filter(({ c }) => c.legacy !== 1);
3598
- const v2Total = v2Compartments.length;
3599
- const v2IndexByOriginalIndex = new Map;
3600
- const curveInputs = v2Compartments.map(({ c, originalIndex }, v2Ordinal) => {
3601
- const curveIndex = v2Total - v2Ordinal;
3602
- v2IndexByOriginalIndex.set(originalIndex, curveIndex);
3603
- return {
3604
- index: curveIndex,
3605
- importance: Math.max(1, Math.min(100, c.importance ?? 50))
3606
- };
3607
- });
3608
- const pressure = historyBudgetTokens > 0 ? computeBudgetPressure(curveInputs, historyBudgetTokens) : 1;
3609
- return compartments.map((c, index) => {
3610
- if (c.legacy === 1)
3611
- return legacyTier(c);
3612
- return renderedTier(v2IndexByOriginalIndex.get(index) ?? 1, c.importance ?? 50, pressure, 0);
3613
- });
3614
- }
3740
+ // ../plugin/src/web/host-opencode.ts
3741
+ init_opencode_db_path();
3742
+ await init_sqlite();
3743
+ import { existsSync as existsSync5 } from "node:fs";
3615
3744
 
3616
3745
  // ../plugin/src/v2/store-reader.ts
3617
3746
  init_opencode_db_path();
@@ -4069,9 +4198,6 @@ class V2StoreReader {
4069
4198
  }
4070
4199
 
4071
4200
  // ../plugin/src/web/host-opencode.ts
4072
- init_opencode_db_path();
4073
- await init_sqlite();
4074
- import { existsSync as existsSync5 } from "node:fs";
4075
4201
  function openOc2HostStore(env = process.env) {
4076
4202
  const resolution = resolveOpenCodeDbPath("v2", { env });
4077
4203
  if (resolution.path !== ":memory:" && !existsSync5(resolution.path)) {
@@ -4155,6 +4281,546 @@ function numberOf(value) {
4155
4281
  return typeof value === "number" && Number.isFinite(value) ? value : null;
4156
4282
  }
4157
4283
 
4284
+ // ../plugin/src/web/display-path.ts
4285
+ function resolveProjectDisplayPaths(sources, projects, sessions) {
4286
+ const oc2Directories = oc2DirectoryIndex(sources);
4287
+ const piDirectories = piDirectoryIndex(sources);
4288
+ const candidatesByProject = new Map;
4289
+ for (const session of sessions) {
4290
+ if (session.harness === "pi") {
4291
+ for (const directory of piDirectories.get(session.sessionId) ?? []) {
4292
+ pushCandidate(candidatesByProject, session.projectPath, {
4293
+ directory,
4294
+ activityAt: session.lastActivityAt,
4295
+ sessionId: session.sessionId
4296
+ });
4297
+ }
4298
+ continue;
4299
+ }
4300
+ const host = oc2Directories.get(session.sessionId);
4301
+ if (!host)
4302
+ continue;
4303
+ pushCandidate(candidatesByProject, session.projectPath, {
4304
+ directory: host.directory,
4305
+ activityAt: host.timeUpdated ?? session.lastActivityAt,
4306
+ sessionId: session.sessionId
4307
+ });
4308
+ }
4309
+ const resolved = new Map;
4310
+ for (const project of projects) {
4311
+ const candidates = candidatesByProject.get(project.projectPath);
4312
+ if (candidates && candidates.length > 0) {
4313
+ resolved.set(project.projectPath, pickBestCandidate(candidates).directory);
4314
+ continue;
4315
+ }
4316
+ resolved.set(project.projectPath, isAbsolute3(project.projectPath) ? project.projectPath : null);
4317
+ }
4318
+ return resolved;
4319
+ }
4320
+ function oc2DirectoryIndex(sources) {
4321
+ const index = new Map;
4322
+ try {
4323
+ const opened = openOc2HostStore(sources.oc2Env ?? process.env);
4324
+ if (!opened.store)
4325
+ return index;
4326
+ try {
4327
+ for (const meta of listOc2SessionMeta(opened.store)) {
4328
+ if (meta.directory && meta.directory.trim().length > 0) {
4329
+ index.set(meta.sessionId, {
4330
+ directory: meta.directory,
4331
+ timeUpdated: meta.timeUpdated
4332
+ });
4333
+ }
4334
+ }
4335
+ } catch {} finally {
4336
+ opened.store.close();
4337
+ }
4338
+ } catch {}
4339
+ return index;
4340
+ }
4341
+ function piDirectoryIndex(sources) {
4342
+ const index = new Map;
4343
+ try {
4344
+ const listing = listPiSessionFiles(sources.piSessionsRoot ?? piSessionsRoot());
4345
+ for (const group of listing.groups) {
4346
+ const directories = new Set;
4347
+ for (const file of group.files) {
4348
+ if (file.headerCwd && file.headerCwd.trim().length > 0) {
4349
+ directories.add(file.headerCwd);
4350
+ }
4351
+ }
4352
+ if (directories.size > 0)
4353
+ index.set(group.sessionId, directories);
4354
+ }
4355
+ } catch {}
4356
+ return index;
4357
+ }
4358
+ function pushCandidate(byProject, projectPath, candidate) {
4359
+ const list = byProject.get(projectPath);
4360
+ if (list)
4361
+ list.push(candidate);
4362
+ else
4363
+ byProject.set(projectPath, [candidate]);
4364
+ }
4365
+ function pickBestCandidate(candidates) {
4366
+ return [...candidates].sort(compareCandidates)[0];
4367
+ }
4368
+ function compareCandidates(a, b) {
4369
+ if (a.activityAt !== b.activityAt) {
4370
+ if (a.activityAt === null)
4371
+ return 1;
4372
+ if (b.activityAt === null)
4373
+ return -1;
4374
+ return b.activityAt - a.activityAt;
4375
+ }
4376
+ if (a.sessionId !== b.sessionId)
4377
+ return a.sessionId < b.sessionId ? -1 : 1;
4378
+ return a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0;
4379
+ }
4380
+
4381
+ // ../plugin/src/hooks/magic-context/decay-curve.ts
4382
+ var H50 = 24;
4383
+ var D = 25;
4384
+ var G = 2;
4385
+ var Z1 = 0.201;
4386
+ var Z4 = 2.587;
4387
+ var P_FLOOR = 0.1;
4388
+ var TIER_COST = [0, 322, 109, 35, 20, 5];
4389
+ function tier(compartmentIndex, importance, budgetPressure) {
4390
+ const a = Math.max(compartmentIndex, 1) - 1;
4391
+ const imp = Math.max(1, Math.min(100, importance));
4392
+ const p = Math.max(budgetPressure, P_FLOOR);
4393
+ const F = 2 ** ((imp - 50) / D);
4394
+ const H = H50 * F / p;
4395
+ const z = a / H;
4396
+ if (z < Z1)
4397
+ return 1;
4398
+ if (z < Z4)
4399
+ return 4;
4400
+ return 5;
4401
+ }
4402
+ function shouldArchive(compartmentIndex, importance, budgetPressure, anchorOverlap = 0) {
4403
+ const a = Math.max(compartmentIndex, 1) - 1;
4404
+ const imp = Math.max(1, Math.min(100, importance));
4405
+ const p = Math.max(budgetPressure, P_FLOOR);
4406
+ const o = Math.max(0, Math.min(1, anchorOverlap));
4407
+ const F = 2 ** ((imp - 50) / D);
4408
+ const H = H50 * F / p;
4409
+ const z = a / H;
4410
+ return z >= Z4 + G * o;
4411
+ }
4412
+ function renderedTier(compartmentIndex, importance, budgetPressure, anchorOverlap = 0) {
4413
+ if (shouldArchive(compartmentIndex, importance, budgetPressure, anchorOverlap)) {
4414
+ return 5;
4415
+ }
4416
+ const base = tier(compartmentIndex, importance, budgetPressure);
4417
+ return Math.min(base, 4);
4418
+ }
4419
+ function computeBudgetPressure(compartments, historyBudget) {
4420
+ if (historyBudget <= 0)
4421
+ return 1;
4422
+ let naturalCost = 0;
4423
+ for (const c of compartments) {
4424
+ const naturalTier = tier(c.index, c.importance, 1);
4425
+ naturalCost += naturalTier >= 5 ? 0 : TIER_COST[naturalTier];
4426
+ }
4427
+ return Math.max(P_FLOOR, naturalCost / historyBudget);
4428
+ }
4429
+
4430
+ // ../plugin/src/hooks/magic-context/decay-render.ts
4431
+ init_read_session_formatting();
4432
+ var DEFAULT_HISTORY_BUDGET_TOKENS = 60000;
4433
+ function legacyTier(c) {
4434
+ return /^U:/m.test(c.content) ? 3 : 4;
4435
+ }
4436
+ function computeTiers(compartments, historyBudgetTokens) {
4437
+ const v2Compartments = compartments.map((c, originalIndex) => ({ c, originalIndex })).filter(({ c }) => c.legacy !== 1);
4438
+ const v2Total = v2Compartments.length;
4439
+ const v2IndexByOriginalIndex = new Map;
4440
+ const curveInputs = v2Compartments.map(({ c, originalIndex }, v2Ordinal) => {
4441
+ const curveIndex = v2Total - v2Ordinal;
4442
+ v2IndexByOriginalIndex.set(originalIndex, curveIndex);
4443
+ return {
4444
+ index: curveIndex,
4445
+ importance: Math.max(1, Math.min(100, c.importance ?? 50))
4446
+ };
4447
+ });
4448
+ const pressure = historyBudgetTokens > 0 ? computeBudgetPressure(curveInputs, historyBudgetTokens) : 1;
4449
+ return compartments.map((c, index) => {
4450
+ if (c.legacy === 1)
4451
+ return legacyTier(c);
4452
+ return renderedTier(v2IndexByOriginalIndex.get(index) ?? 1, c.importance ?? 50, pressure, 0);
4453
+ });
4454
+ }
4455
+ // ../plugin/src/v2/fold/restore.ts
4456
+ import { fileURLToPath } from "node:url";
4457
+
4458
+ // ../plugin/src/v2/hooks/storage-notice.ts
4459
+ var STORAGE_NOTICE_PREFIX = "[Magic Context storage notice] ";
4460
+ function isStorageNoticeText(text) {
4461
+ return typeof text === "string" && text.startsWith(STORAGE_NOTICE_PREFIX);
4462
+ }
4463
+
4464
+ // ../plugin/src/v2/fold/restore.ts
4465
+ function unavailableAttachmentNote(name, mediaType) {
4466
+ return `[Attachment ${name ? `"${name}" ` : ""}(${mediaType}) is not available in this restored history]`;
4467
+ }
4468
+ var imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
4469
+ function attachmentParts(files, rowID, media) {
4470
+ const seen = new Map;
4471
+ return files.flatMap((file) => {
4472
+ if (imageMimes.has(file.mime) && file.source.type === "inline" && file.mention?.text) {
4473
+ const key = JSON.stringify([
4474
+ file.mime,
4475
+ file.name ?? null,
4476
+ file.description ?? null,
4477
+ file.mention.text
4478
+ ]);
4479
+ const payloads = seen.get(key) ?? new Set;
4480
+ if (payloads.has(file.data))
4481
+ return [];
4482
+ payloads.add(file.data);
4483
+ seen.set(key, payloads);
4484
+ }
4485
+ const uri = file.source.type === "uri" ? file.source.uri : undefined;
4486
+ const location = uri?.startsWith("file:") ? fileURLToPath(uri) : undefined;
4487
+ if (file.mime === "text/plain" || file.mime === "application/x-directory") {
4488
+ const directory = file.mime === "application/x-directory";
4489
+ return [
4490
+ {
4491
+ type: "text",
4492
+ text: `
4493
+
4494
+ ${[
4495
+ directory ? `Attached directory: ${location ?? file.name ?? uri ?? "directory"}` : `Attached file: ${file.name ?? uri ?? "inline attachment"}`,
4496
+ file.description === undefined ? undefined : `Description: ${file.description}`,
4497
+ directory && !file.data.length ? undefined : "",
4498
+ directory && !file.data.length ? undefined : Buffer.from(file.data, "base64").toString("utf8")
4499
+ ].filter((line) => line !== undefined).join(`
4500
+ `)}`,
4501
+ metadata: {
4502
+ attachment: {
4503
+ source: file.source,
4504
+ name: file.name,
4505
+ description: file.description
4506
+ }
4507
+ }
4508
+ }
4509
+ ];
4510
+ }
4511
+ if (!imageMimes.has(file.mime) && file.mime !== "application/pdf")
4512
+ return [];
4513
+ const metadata = file.description === undefined ? undefined : { description: file.description };
4514
+ const located = location ? [{ type: "text", text: `Attached file: ${location}` }] : [];
4515
+ if (!media)
4516
+ return [
4517
+ ...located,
4518
+ {
4519
+ type: "media",
4520
+ mediaType: file.mime,
4521
+ data: file.data,
4522
+ filename: file.name,
4523
+ metadata
4524
+ }
4525
+ ];
4526
+ const asset = media.asset(file.data, file.mime);
4527
+ if (typeof asset === "string") {
4528
+ media.unavailable({ rowID, name: file.name, mediaType: file.mime, reason: asset });
4529
+ return [
4530
+ ...located,
4531
+ { type: "text", text: unavailableAttachmentNote(file.name, file.mime) }
4532
+ ];
4533
+ }
4534
+ return [...located, { type: "media", media: asset, filename: file.name, metadata }];
4535
+ });
4536
+ }
4537
+ function restoreRow(row, model, media) {
4538
+ const data = row.data;
4539
+ const make = (role, content, metadata = data.metadata) => ({
4540
+ id: row.id,
4541
+ role,
4542
+ content,
4543
+ ...metadata === undefined ? {} : { metadata }
4544
+ });
4545
+ if (row.type === "user") {
4546
+ const skills = data.skills ?? [];
4547
+ const content = [
4548
+ ...skills.flatMap((skill) => skill.text === undefined ? [] : [{ type: "text", text: skill.text }]),
4549
+ ...data.text ? [{ type: "text", text: data.text }] : [],
4550
+ ...attachmentParts(data.files ?? [], row.id, media)
4551
+ ];
4552
+ return content.length ? [
4553
+ make("user", content, {
4554
+ ...data.metadata ?? {},
4555
+ ...data.agents?.length ? { agents: data.agents } : {}
4556
+ })
4557
+ ] : [];
4558
+ }
4559
+ if (row.type === "system")
4560
+ return [{ role: "system", content: [{ type: "text", text: data.text ?? "" }] }];
4561
+ if (row.type === "synthetic" && isStorageNoticeText(data.text))
4562
+ return [];
4563
+ if (["synthetic", "skill"].includes(row.type))
4564
+ return [make("user", [{ type: "text", text: data.text ?? "" }])];
4565
+ if (row.type === "location-switched")
4566
+ return [
4567
+ make("user", [
4568
+ {
4569
+ type: "text",
4570
+ text: `The working directory has been changed to ${data.location.directory}.`
4571
+ }
4572
+ ])
4573
+ ];
4574
+ if (row.type === "shell")
4575
+ return data.metadata?.background === true ? [] : [
4576
+ make("user", [
4577
+ {
4578
+ type: "text",
4579
+ text: `The following shell command was executed by the user:
4580
+
4581
+ Command:
4582
+ ${data.command}
4583
+
4584
+ Output:
4585
+ ${data.output?.output ?? ""}`
4586
+ }
4587
+ ])
4588
+ ];
4589
+ if (row.type !== "assistant")
4590
+ return [];
4591
+ const previousModel = data.model;
4592
+ const sameProvider = previousModel?.providerID === model.providerID;
4593
+ const sameModel = sameProvider && previousModel?.id === model.id;
4594
+ const reuse = sameModel && data.error === undefined;
4595
+ const metadata = (state) => state === undefined ? undefined : { [model.providerID]: state };
4596
+ const content = [];
4597
+ const results = [];
4598
+ for (const part of data.content ?? []) {
4599
+ if (part.type === "text") {
4600
+ if (part.text !== "")
4601
+ content.push({
4602
+ type: "text",
4603
+ text: part.text,
4604
+ providerMetadata: reuse ? metadata(part.state) : undefined
4605
+ });
4606
+ continue;
4607
+ }
4608
+ if (part.type === "reasoning") {
4609
+ if (part.text !== "" || reuse && part.state !== undefined)
4610
+ content.push({
4611
+ type: reuse || data.error === undefined ? "reasoning" : "text",
4612
+ text: part.text,
4613
+ providerMetadata: reuse ? metadata(part.state) : undefined
4614
+ });
4615
+ continue;
4616
+ }
4617
+ const state = part.state;
4618
+ const completed = state.status === "completed" || state.status === "error";
4619
+ const toolReuse = reuse || sameModel && part.executed === true && completed;
4620
+ let input = state.input;
4621
+ if (state.status === "streaming" && typeof input === "string") {
4622
+ try {
4623
+ input = JSON.parse(input);
4624
+ } catch {}
4625
+ }
4626
+ content.push({
4627
+ type: "tool-call",
4628
+ id: part.id,
4629
+ name: part.name,
4630
+ input,
4631
+ providerExecuted: part.executed,
4632
+ providerMetadata: toolReuse ? metadata(part.providerState) : undefined
4633
+ });
4634
+ if (!completed)
4635
+ continue;
4636
+ const toolContent = state.content ?? [];
4637
+ const single = toolContent.length === 1 ? toolContent[0] : undefined;
4638
+ const result = {
4639
+ type: "tool-result",
4640
+ id: part.id,
4641
+ name: part.name,
4642
+ providerExecuted: part.executed,
4643
+ providerMetadata: toolReuse ? metadata(part.providerResultState ?? part.providerState) : sameProvider ? metadata(part.providerResultState) : undefined,
4644
+ result: state.status === "error" ? { error: state.error, content: toolContent } : single?.type === "text" ? { type: "text", value: single.text } : { type: "content", value: toolContent },
4645
+ ...state.status === "error" ? { resultType: "error" } : {}
4646
+ };
4647
+ if (part.executed === true)
4648
+ content.push(result);
4649
+ else
4650
+ results.push({ role: "tool", content: [result] });
4651
+ }
4652
+ return [...content.length ? [make("assistant", content)] : [], ...results];
4653
+ }
4654
+
4655
+ // ../plugin/src/v2/tool-result.ts
4656
+ function toolErrorText(result) {
4657
+ const value = result.value === undefined ? result : result.value;
4658
+ return typeof value === "string" ? value : JSON.stringify(value);
4659
+ }
4660
+ function toolStateContent(state) {
4661
+ if (typeof state.output === "string")
4662
+ return state.output;
4663
+ if (state.status === "error")
4664
+ return toolErrorText({ error: state.error, content: state.content ?? [] });
4665
+ if (typeof state.content === "string")
4666
+ return state.content;
4667
+ if (!Array.isArray(state.content))
4668
+ return "";
4669
+ return state.content.map((part) => {
4670
+ if (typeof part === "string")
4671
+ return part;
4672
+ if (!part || typeof part !== "object")
4673
+ return "";
4674
+ const value = part;
4675
+ if (typeof value.text === "string")
4676
+ return value.text;
4677
+ if (typeof value.value === "string")
4678
+ return value.value;
4679
+ return "";
4680
+ }).filter(Boolean).join(`
4681
+ `);
4682
+ }
4683
+
4684
+ // ../plugin/src/v2/hooks/store.ts
4685
+ var rawMessageTypes = new Set(RAW_MESSAGE_TYPES);
4686
+ var isRawRow = (row) => rawMessageTypes.has(row.type);
4687
+ var NO_MODEL = { providerID: "", id: "" };
4688
+ function hostServesRowById(row) {
4689
+ return isRawRow(row) && restoreRow(row, NO_MODEL).some((message) => message.id === row.id);
4690
+ }
4691
+ function projectRawMessages(rows, ordinalFor) {
4692
+ return rows.filter(isRawRow).map((row, index) => {
4693
+ const message = {
4694
+ id: row.id,
4695
+ ordinal: ordinalFor(row, index),
4696
+ role: row.type === "assistant" ? "assistant" : "user",
4697
+ createdAt: row.time_created ?? row.data.time?.created,
4698
+ parts: row.type === "assistant" ? (row.data.content ?? []).map((part) => {
4699
+ if (part.type !== "tool")
4700
+ return { ...part };
4701
+ const state = part.state;
4702
+ const content = state.content;
4703
+ return {
4704
+ type: "tool",
4705
+ tool: part.name,
4706
+ callID: part.id,
4707
+ state: {
4708
+ ...state,
4709
+ output: state.status === "error" ? toolStateContent(state) : content?.filter((p) => p.type === "text").map((p) => p.text).join(`
4710
+ `) ?? ""
4711
+ }
4712
+ };
4713
+ }) : [{ type: "text", text: row.data.text ?? "" }]
4714
+ };
4715
+ Object.defineProperty(message, "storeType", { value: row.type, enumerable: false });
4716
+ if (!hostServesRowById(row))
4717
+ markHostUnservedRow(message);
4718
+ return message;
4719
+ });
4720
+ }
4721
+ function rawMessagePage(rows, afterOrdinal) {
4722
+ return projectRawMessages(rows, (_row, index) => afterOrdinal + index + 1);
4723
+ }
4724
+
4725
+ // ../plugin/src/web/message-stats.ts
4726
+ var SWEEP_PAGE_ROWS = 1000;
4727
+ var rawMessageTypes2 = new Set(RAW_MESSAGE_TYPES);
4728
+ function codePointLength(text) {
4729
+ let count = 0;
4730
+ for (const _character of text)
4731
+ count += 1;
4732
+ return count;
4733
+ }
4734
+ function partsCharCount(parts) {
4735
+ if (!Array.isArray(parts))
4736
+ return 0;
4737
+ let total = 0;
4738
+ for (const part of parts) {
4739
+ if (!part || typeof part !== "object")
4740
+ continue;
4741
+ const record = part;
4742
+ if (typeof record.text === "string")
4743
+ total += codePointLength(record.text);
4744
+ const output = record.state?.output;
4745
+ if (typeof output === "string")
4746
+ total += codePointLength(output);
4747
+ }
4748
+ return total;
4749
+ }
4750
+ function sweepRangeCounts(page, queries) {
4751
+ const counts = new Map;
4752
+ if (queries.length === 0)
4753
+ return counts;
4754
+ const points = new Set;
4755
+ for (const query of queries) {
4756
+ points.add(query.end);
4757
+ points.add(query.start - 1);
4758
+ }
4759
+ const sorted = [...points].sort((left, right) => left - right);
4760
+ const through = sorted[sorted.length - 1];
4761
+ const cumulative = new Map;
4762
+ let pointIndex = 0;
4763
+ let total = 0;
4764
+ let cursor = -1;
4765
+ const settlePassedPoints = (rowSeq) => {
4766
+ while (pointIndex < sorted.length && sorted[pointIndex] < rowSeq) {
4767
+ cumulative.set(sorted[pointIndex], total);
4768
+ pointIndex += 1;
4769
+ }
4770
+ };
4771
+ for (;; ) {
4772
+ const batch = page(cursor, through);
4773
+ const rawRows = batch.rows.filter((row) => rawMessageTypes2.has(row.type));
4774
+ const rowCounts = new Map;
4775
+ for (const message of rawMessagePage(rawRows, 0)) {
4776
+ rowCounts.set(message.id, partsCharCount(message.parts));
4777
+ }
4778
+ for (const row of batch.rows) {
4779
+ settlePassedPoints(row.seq);
4780
+ const rowCount = rowCounts.get(row.id);
4781
+ if (rowCount !== undefined)
4782
+ total += rowCount;
4783
+ }
4784
+ if (batch.rows.length === 0 || batch.cursor === undefined)
4785
+ break;
4786
+ cursor = batch.cursor;
4787
+ }
4788
+ while (pointIndex < sorted.length) {
4789
+ cumulative.set(sorted[pointIndex], total);
4790
+ pointIndex += 1;
4791
+ }
4792
+ for (const query of queries) {
4793
+ counts.set(query.key, (cumulative.get(query.end) ?? 0) - (cumulative.get(query.start - 1) ?? 0));
4794
+ }
4795
+ return counts;
4796
+ }
4797
+ function buildOrdinalPrefix(messages) {
4798
+ const ordinals = [];
4799
+ const cumulative = [];
4800
+ let total = 0;
4801
+ for (const message of messages) {
4802
+ total += partsCharCount(message.parts);
4803
+ ordinals.push(message.ordinal);
4804
+ cumulative.push(total);
4805
+ }
4806
+ return { ordinals, cumulative };
4807
+ }
4808
+ function ordinalRangeCount(prefix, start, end) {
4809
+ return cumulativeThrough(prefix, end) - cumulativeThrough(prefix, start - 1);
4810
+ }
4811
+ function cumulativeThrough(prefix, through) {
4812
+ let low = 0;
4813
+ let high = prefix.ordinals.length;
4814
+ while (low < high) {
4815
+ const middle = low + high >> 1;
4816
+ if (prefix.ordinals[middle] <= through)
4817
+ low = middle + 1;
4818
+ else
4819
+ high = middle;
4820
+ }
4821
+ return low === 0 ? 0 : prefix.cumulative[low - 1];
4822
+ }
4823
+
4158
4824
  // ../plugin/src/web/views.ts
4159
4825
  function openContextFor(sources) {
4160
4826
  const dbPath = sources.contextDbPath ?? resolveDatabasePath().dbPath;
@@ -4266,10 +4932,11 @@ function oc2MessagePage(sources, sessionId, options) {
4266
4932
  const limit = clampLimit(options.limit);
4267
4933
  const outputLimit = options.fullOutputs ? undefined : DEFAULT_OUTPUT_LIMIT;
4268
4934
  const kept = [];
4935
+ const end = options.end;
4269
4936
  let after = options.after ?? -1;
4270
4937
  let hasMore = false;
4271
4938
  for (let guard = 0;guard < 200; guard++) {
4272
- const page = reader.page(sessionId, { after, limit: 200 });
4939
+ const page = reader.page(sessionId, { after, limit: 200, through: end });
4273
4940
  for (const row of page.rows) {
4274
4941
  if (!isRawType(row.type))
4275
4942
  continue;
@@ -4284,6 +4951,7 @@ function oc2MessagePage(sources, sessionId, options) {
4284
4951
  after = page.cursor;
4285
4952
  }
4286
4953
  const last = kept.at(-1);
4954
+ const partsById = new Map(rawMessagePage(kept, 0).map((message) => [message.id, message.parts]));
4287
4955
  return {
4288
4956
  source: "oc2-host",
4289
4957
  rows: kept.map((row) => ({
@@ -4291,7 +4959,7 @@ function oc2MessagePage(sources, sessionId, options) {
4291
4959
  id: row.id,
4292
4960
  role: row.type,
4293
4961
  timeCreated: typeof row.time_created === "number" ? row.time_created : null,
4294
- parts: truncateParts(row.data.parts, outputLimit)
4962
+ parts: truncateParts(partsById.get(row.id), outputLimit)
4295
4963
  })),
4296
4964
  nextAfter: last ? last.seq : options.after ?? null,
4297
4965
  hasMore
@@ -4316,7 +4984,8 @@ function piMessagePage(sources, sessionId, options) {
4316
4984
  const limit = clampLimit(options.limit);
4317
4985
  const outputLimit = options.fullOutputs ? undefined : DEFAULT_OUTPUT_LIMIT;
4318
4986
  const after = options.after ?? 0;
4319
- const selected = projection.messages.filter((message) => message.ordinal > after);
4987
+ const end = options.end;
4988
+ const selected = projection.messages.filter((message) => message.ordinal > after && (end === undefined || message.ordinal <= end));
4320
4989
  const page = selected.slice(0, limit);
4321
4990
  return {
4322
4991
  source: "pi-jsonl",
@@ -4373,7 +5042,8 @@ function listSessionCompartmentsView(sources, projectPath, sessionId, options) {
4373
5042
  throw new ContextStoreUnavailableError(opened.status);
4374
5043
  try {
4375
5044
  const rows = opened.store.db.prepare("SELECT id, sequence, title, importance, created_at, episode_type, legacy, content, p1, p2, p3, p4, start_message_id, end_message_id FROM compartments WHERE session_id = ? AND harness = ? ORDER BY sequence ASC").all(sessionId, harness);
4376
- const anchors = harness === "pi" ? piAnchors(sources, sessionId, rows, options.branch) : oc2Anchors(sources, sessionId);
5045
+ const stats = harness === "pi" ? piAnchorsAndCounts(sources, sessionId, rows, options.branch) : oc2AnchorsAndCounts(sources, sessionId, rows);
5046
+ const anchors = stats.anchors;
4377
5047
  const tiers = computeTiers(rows.map((row) => ({
4378
5048
  startMessage: 0,
4379
5049
  endMessage: 0,
@@ -4395,6 +5065,7 @@ function listSessionCompartmentsView(sources, projectPath, sessionId, options) {
4395
5065
  legacy: row.legacy === 1,
4396
5066
  episodeType: row.episode_type ?? null,
4397
5067
  content: row.content,
5068
+ summaryCharCount: codePointLength(row.content),
4398
5069
  details: { p1: row.p1, p2: row.p2, p3: row.p3, p4: row.p4 },
4399
5070
  startMessageId: row.start_message_id,
4400
5071
  endMessageId: row.end_message_id,
@@ -4402,6 +5073,7 @@ function listSessionCompartmentsView(sources, projectPath, sessionId, options) {
4402
5073
  start: boundaryFor(row.start_message_id, anchors),
4403
5074
  end: boundaryFor(row.end_message_id, anchors)
4404
5075
  },
5076
+ messageCharCount: stats.counts.get(row.id) ?? null,
4405
5077
  renderTier: tiers[index] <= 1 ? "full" : "decayed"
4406
5078
  }));
4407
5079
  } finally {
@@ -4413,32 +5085,52 @@ function boundaryFor(messageId, anchors) {
4413
5085
  return { state: "unlinked" };
4414
5086
  return anchors.get(messageId) ?? { state: "unresolved" };
4415
5087
  }
4416
- function oc2Anchors(sources, sessionId) {
5088
+ function resolvedAnchor(boundary) {
5089
+ return boundary.state === "resolved" || boundary.state === "synth-resolved" ? boundary.anchor : null;
5090
+ }
5091
+ function oc2AnchorsAndCounts(sources, sessionId, rows) {
5092
+ const counts = new Map(rows.map((row) => [row.id, null]));
4417
5093
  const anchors = new Map;
4418
5094
  const opened = openOc2For(sources);
4419
5095
  if (!opened.store)
4420
- return anchors;
5096
+ return { anchors, counts };
4421
5097
  try {
4422
- const rows = opened.store.metaDb.prepare("SELECT id, seq FROM session_message WHERE session_id = ?").all(sessionId);
4423
- for (const row of rows)
4424
- anchors.set(row.id, { state: "resolved", anchor: row.seq });
5098
+ const messages = opened.store.metaDb.prepare("SELECT id, seq FROM session_message WHERE session_id = ?").all(sessionId);
5099
+ for (const message of messages) {
5100
+ anchors.set(message.id, { state: "resolved", anchor: message.seq });
5101
+ }
5102
+ const queries = [];
5103
+ for (const row of rows) {
5104
+ const start = resolvedAnchor(boundaryFor(row.start_message_id, anchors));
5105
+ const end = resolvedAnchor(boundaryFor(row.end_message_id, anchors));
5106
+ if (start === null || end === null || start > end)
5107
+ continue;
5108
+ queries.push({ key: row.id, start, end });
5109
+ }
5110
+ if (queries.length > 0) {
5111
+ const reader = opened.store.reader;
5112
+ const computed = sweepRangeCounts((after, through) => reader.page(sessionId, { after, through, limit: SWEEP_PAGE_ROWS }), queries);
5113
+ for (const [key, count] of computed)
5114
+ counts.set(key, count);
5115
+ }
4425
5116
  } catch {} finally {
4426
5117
  opened.store.close();
4427
5118
  }
4428
- return anchors;
5119
+ return { anchors, counts };
4429
5120
  }
4430
- function piAnchors(sources, sessionId, rows, branch) {
5121
+ function piAnchorsAndCounts(sources, sessionId, rows, branch) {
4431
5122
  const anchors = new Map;
5123
+ const counts = new Map(rows.map((row) => [row.id, null]));
4432
5124
  const converter = sources.piConverter;
4433
5125
  if (!converter)
4434
- return anchors;
5126
+ return { anchors, counts };
4435
5127
  const group = resolvePiBranchGroup(sources, sessionId, branch);
4436
5128
  const file = group.files[0];
4437
5129
  if (!file)
4438
- return anchors;
5130
+ return { anchors, counts };
4439
5131
  const projection = projectPiSessionFile(file.filePath, converter);
4440
5132
  if (projection.status.kind !== "ok")
4441
- return anchors;
5133
+ return { anchors, counts };
4442
5134
  const aliases = sources.piAliasBuilder?.(projection.entries);
4443
5135
  for (const row of rows) {
4444
5136
  for (const messageId of [row.start_message_id, row.end_message_id]) {
@@ -4450,7 +5142,15 @@ function piAnchors(sources, sessionId, rows, branch) {
4450
5142
  anchors.set(messageId, boundaryFromResolution(resolved));
4451
5143
  }
4452
5144
  }
4453
- return anchors;
5145
+ const prefix = buildOrdinalPrefix(projection.messages);
5146
+ for (const row of rows) {
5147
+ const start = resolvedAnchor(boundaryFor(row.start_message_id, anchors));
5148
+ const end = resolvedAnchor(boundaryFor(row.end_message_id, anchors));
5149
+ if (start === null || end === null || start > end)
5150
+ continue;
5151
+ counts.set(row.id, ordinalRangeCount(prefix, start, end));
5152
+ }
5153
+ return { anchors, counts };
4454
5154
  }
4455
5155
  function boundaryFromResolution(resolution) {
4456
5156
  if (resolution.state === "resolved") {
@@ -4640,13 +5340,20 @@ function sourceStatuses(sources) {
4640
5340
  }
4641
5341
  function projectsHandler(sources) {
4642
5342
  return (request) => {
5343
+ const fixtureMode = sources.fixtureMode === true;
4643
5344
  const opened = openContextStoreReadonly(sources.contextDbPath ?? requireContextPath(sources));
4644
5345
  if (!opened.store)
4645
- return ok({ sources: sourceStatuses(sources), projects: [] }, request.now);
5346
+ return ok({ sources: sourceStatuses(sources), fixtureMode, projects: [] }, request.now);
4646
5347
  try {
5348
+ const projects = listWebProjects(opened.store);
5349
+ const displayPaths = resolveProjectDisplayPaths(sources, projects, listProjectSessionRefs(opened.store));
4647
5350
  return ok({
4648
5351
  sources: sourceStatuses(sources),
4649
- projects: listWebProjects(opened.store)
5352
+ fixtureMode,
5353
+ projects: projects.map((project) => ({
5354
+ ...project,
5355
+ displayPath: displayPaths.get(project.projectPath) ?? null
5356
+ }))
4650
5357
  }, request.now);
4651
5358
  } finally {
4652
5359
  opened.store.db.close();
@@ -4670,6 +5377,7 @@ function messagesHandler(sources) {
4670
5377
  const page = listSessionMessages(sources, request.projectPath, sessionId, {
4671
5378
  branch: optionalQuery(request.query, "branch"),
4672
5379
  after: optionalInt(request.query, "after"),
5380
+ end: optionalNonNegativeInt(request.query, "end"),
4673
5381
  limit: optionalInt(request.query, "limit"),
4674
5382
  fullOutputs: request.query.get("fullOutputs") === "1"
4675
5383
  });
@@ -4802,6 +5510,13 @@ function optionalInt(query, name) {
4802
5510
  }
4803
5511
  return parsed;
4804
5512
  }
5513
+ function optionalNonNegativeInt(query, name) {
5514
+ const parsed = optionalInt(query, name);
5515
+ if (parsed !== undefined && parsed < 0) {
5516
+ throw new BadRequestError(`${name} must be a non-negative integer`);
5517
+ }
5518
+ return parsed;
5519
+ }
4805
5520
  function badRequest(message) {
4806
5521
  return { status: 400, body: { error: { code: "bad-request", message } } };
4807
5522
  }
@@ -5497,7 +6212,7 @@ var CONTENT_TYPES = {
5497
6212
  ".map": "application/json; charset=utf-8"
5498
6213
  };
5499
6214
  function resolveDefaultUiRoot() {
5500
- return fileURLToPath(new URL("./ui/", import.meta.url));
6215
+ return fileURLToPath2(new URL("./ui/", import.meta.url));
5501
6216
  }
5502
6217
  function startWebServer(options = {}) {
5503
6218
  const host = options.host ?? DEFAULT_WEB_HOST;
@@ -6258,8 +6973,18 @@ function main() {
6258
6973
  `);
6259
6974
  }
6260
6975
  var args = process.argv.slice(2);
6976
+ var parsed = parseWebEntryArgs(args);
6977
+ if (parsed.kind === "help") {
6978
+ process.stdout.write(webEntryUsage());
6979
+ process.exit(0);
6980
+ }
6981
+ if (parsed.kind === "usage-error") {
6982
+ process.stderr.write(`web panel: ${parsed.message}
6983
+
6984
+ ${webEntryUsage()}`);
6985
+ process.exit(2);
6986
+ }
6261
6987
  try {
6262
- const parsed = parseEntryArgs(args);
6263
6988
  if (parsed.mode === "smoke") {
6264
6989
  main();
6265
6990
  } else {
@@ -6287,6 +7012,7 @@ async function runServer(parsed) {
6287
7012
  }
6288
7013
  sources.piConverter = convertEntriesToRawMessages;
6289
7014
  sources.piAliasBuilder = buildPiSynthUserIdAlias;
7015
+ sources.fixtureMode = parsed.fixture;
6290
7016
  const handle = await startWebServer({ port: parsed.port, sources });
6291
7017
  process.stdout.write(`mmc web panel (@retaw106/mmc-pi) listening on ${handle.url}
6292
7018
  ` + (fixture ? `fixture root: ${fixture.root}
@@ -6294,29 +7020,3 @@ async function runServer(parsed) {
6294
7020
  ` + `press Ctrl+C to stop
6295
7021
  `);
6296
7022
  }
6297
- function parseEntryArgs(args) {
6298
- let port;
6299
- const portFlag = args.indexOf("--port");
6300
- if (portFlag >= 0) {
6301
- const value = Number(args[portFlag + 1]);
6302
- if (!Number.isSafeInteger(value) || value < 0 || value > 65535) {
6303
- throw new Error("--port must be an integer in [0, 65535]");
6304
- }
6305
- port = value;
6306
- } else if (process.env.MMC_WEB_PORT) {
6307
- const value = Number(process.env.MMC_WEB_PORT);
6308
- if (Number.isSafeInteger(value) && value >= 0 && value <= 65535)
6309
- port = value;
6310
- }
6311
- const fixtureRootFlag = args.indexOf("--fixture-root");
6312
- const fixtureRoot = fixtureRootFlag >= 0 ? args[fixtureRootFlag + 1] : undefined;
6313
- if (fixtureRootFlag >= 0 && !fixtureRoot) {
6314
- throw new Error("--fixture-root requires a directory argument");
6315
- }
6316
- return {
6317
- mode: args.includes("--smoke") ? "smoke" : "serve",
6318
- port,
6319
- fixture: args.includes("--fixture"),
6320
- fixtureRoot
6321
- };
6322
- }