chat-recall 0.7.23 → 0.7.25

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/mcp.js CHANGED
@@ -6412,6 +6412,130 @@ var init_sqlite_reader = __esm({
6412
6412
  }
6413
6413
  });
6414
6414
 
6415
+ // ../engine/dist/core/backends/opencode-v2.js
6416
+ function hasTable(db, name) {
6417
+ return !!db.prepare("SELECT 1 FROM main.sqlite_master WHERE type = 'table' AND name = ?").get(name);
6418
+ }
6419
+ function columnsOf(db, table) {
6420
+ return new Set(db.prepare(`SELECT name FROM pragma_table_info('${table}', 'main')`).all().map((r) => r.name));
6421
+ }
6422
+ function installOpencodeV2Views(db) {
6423
+ if (!hasTable(db, "session_v2") || !hasTable(db, "session_message"))
6424
+ return false;
6425
+ const v2Cols = columnsOf(db, "session_v2");
6426
+ const sessionCols = SESSION_COLUMNS.map((c) => v2Cols.has(c) ? c : `NULL AS ${c}`).join(", ");
6427
+ const hasV1 = hasTable(db, "message") && hasTable(db, "part");
6428
+ const v1Messages = hasV1 ? `SELECT id, session_id, time_created, time_updated, data FROM main.message WHERE session_id NOT IN (${V2_SESSIONS}) UNION ALL ` : "";
6429
+ const v1Parts = hasV1 ? `SELECT id, message_id, session_id, time_created, time_updated, data FROM main.part WHERE session_id NOT IN (${V2_SESSIONS}) UNION ALL ` : "";
6430
+ db.exec(`
6431
+ CREATE TEMP VIEW IF NOT EXISTS session AS SELECT ${sessionCols} FROM main.session_v2;
6432
+
6433
+ CREATE TEMP VIEW IF NOT EXISTS message AS
6434
+ ${v1Messages}
6435
+ SELECT m.id, m.session_id, m.time_created, m.time_updated,
6436
+ CASE m.type
6437
+ WHEN 'user' THEN json_object('role', 'user', 'time', ${j("$.time")})
6438
+ ELSE json_object(
6439
+ 'role', 'assistant',
6440
+ 'agent', json_extract(m.data, '$.agent'),
6441
+ 'modelID', json_extract(m.data, '$.model.id'),
6442
+ 'providerID', json_extract(m.data, '$.model.providerID'),
6443
+ 'cost', coalesce(json_extract(m.data, '$.cost'), 0),
6444
+ 'tokens', ${j("$.tokens")},
6445
+ 'time', ${j("$.time")})
6446
+ END AS data
6447
+ FROM main.session_message m WHERE m.type IN ('user', 'assistant');
6448
+
6449
+ CREATE TEMP VIEW IF NOT EXISTS part AS
6450
+ ${v1Parts}
6451
+ SELECT m.id || ':text', m.id, m.session_id, m.time_created, m.time_updated,
6452
+ json_object('type', 'text', 'text', json_extract(m.data, '$.text'))
6453
+ FROM main.session_message m
6454
+ WHERE m.type = 'user' AND json_extract(m.data, '$.text') IS NOT NULL
6455
+ UNION ALL
6456
+ SELECT m.id || ':' || c.key, m.id, m.session_id, m.time_created + c.key, m.time_updated,
6457
+ CASE json_extract(c.value, '$.type')
6458
+ WHEN 'tool' THEN json_object(
6459
+ 'type', 'tool',
6460
+ 'tool', json_extract(c.value, '$.name'),
6461
+ 'callID', json_extract(c.value, '$.id'),
6462
+ 'state', json_object(
6463
+ 'status', json_extract(c.value, '$.state.status'),
6464
+ 'input', json(coalesce(json_extract(c.value, '$.state.input'), '{}')),
6465
+ 'output', (SELECT group_concat(json_extract(o.value, '$.text'), char(10))
6466
+ FROM json_each(c.value, '$.state.content') o
6467
+ WHERE json_extract(o.value, '$.type') = 'text')))
6468
+ ELSE json_object('type', json_extract(c.value, '$.type'), 'text', json_extract(c.value, '$.text'))
6469
+ END
6470
+ FROM main.session_message m, json_each(m.data, '$.content') c
6471
+ WHERE m.type = 'assistant'
6472
+ UNION ALL
6473
+ SELECT m.id || ':step', m.id, m.session_id,
6474
+ coalesce(json_extract(m.data, '$.time.completed'), m.time_created), m.time_updated,
6475
+ json_object('type', 'step-finish', 'cost', coalesce(json_extract(m.data, '$.cost'), 0), 'tokens', ${j("$.tokens")})
6476
+ FROM main.session_message m WHERE m.type = 'assistant';
6477
+ `);
6478
+ return true;
6479
+ }
6480
+ function openOpencodeDb(path2) {
6481
+ const db = openSqliteReadonly(path2);
6482
+ if (!db)
6483
+ return null;
6484
+ try {
6485
+ installOpencodeV2Views(db);
6486
+ } catch {
6487
+ }
6488
+ return db;
6489
+ }
6490
+ function openOpencodeDbOrThrow(path2) {
6491
+ const db = openSqliteReadonlyOrThrow(path2);
6492
+ try {
6493
+ installOpencodeV2Views(db);
6494
+ } catch {
6495
+ }
6496
+ return db;
6497
+ }
6498
+ var SESSION_COLUMNS, j, V2_SESSIONS;
6499
+ var init_opencode_v2 = __esm({
6500
+ "../engine/dist/core/backends/opencode-v2.js"() {
6501
+ "use strict";
6502
+ init_sqlite_reader();
6503
+ SESSION_COLUMNS = [
6504
+ "id",
6505
+ "project_id",
6506
+ "parent_id",
6507
+ "slug",
6508
+ "directory",
6509
+ "title",
6510
+ "version",
6511
+ "share_url",
6512
+ "summary_additions",
6513
+ "summary_deletions",
6514
+ "summary_files",
6515
+ "summary_diffs",
6516
+ "revert",
6517
+ "permission",
6518
+ "time_created",
6519
+ "time_updated",
6520
+ "time_compacting",
6521
+ "time_archived",
6522
+ "workspace_id",
6523
+ "path",
6524
+ "agent",
6525
+ "model",
6526
+ "cost",
6527
+ "tokens_input",
6528
+ "tokens_output",
6529
+ "tokens_reasoning",
6530
+ "tokens_cache_read",
6531
+ "tokens_cache_write",
6532
+ "metadata"
6533
+ ];
6534
+ j = (path2, fallback = "'{}'") => `json(coalesce(json_extract(m.data, '${path2}'), ${fallback}))`;
6535
+ V2_SESSIONS = "SELECT session_id FROM main.session_message";
6536
+ }
6537
+ });
6538
+
6415
6539
  // ../engine/dist/core/generic-engine.js
6416
6540
  import { basename as basename5 } from "path";
6417
6541
  import { createPatch } from "diff";
@@ -6709,13 +6833,13 @@ import { existsSync as existsSync7 } from "fs";
6709
6833
  import { homedir as homedir5 } from "os";
6710
6834
  import { dirname as dirname8, join as join9 } from "path";
6711
6835
  function openReadonly(path2) {
6712
- return openSqliteReadonly(path2);
6836
+ return openOpencodeDb(path2);
6713
6837
  }
6714
6838
  var PREFIX, OpencodeBackend, opencodeBackend;
6715
6839
  var init_opencode = __esm({
6716
6840
  "../engine/dist/core/backends/opencode.js"() {
6717
6841
  "use strict";
6718
- init_sqlite_reader();
6842
+ init_opencode_v2();
6719
6843
  init_tool_paths();
6720
6844
  init_session_outcome();
6721
6845
  init_session_git();
@@ -7006,9 +7130,9 @@ var init_opencode = __esm({
7006
7130
  return [];
7007
7131
  const parts = db.prepare(`
7008
7132
  SELECT p.id, p.message_id, p.time_created, p.data, m.data AS msg_data
7009
- FROM part p JOIN message m ON m.id = p.message_id
7133
+ FROM part p JOIN (SELECT id, data FROM message WHERE session_id = ?) m ON m.id = p.message_id
7010
7134
  WHERE p.session_id = ? ORDER BY p.time_created ASC
7011
- `).all(ocId);
7135
+ `).all(ocId, ocId);
7012
7136
  const events = [];
7013
7137
  let lineNum = 0;
7014
7138
  for (const p of parts) {
@@ -7224,17 +7348,17 @@ var init_opencode = __esm({
7224
7348
  if (!existsSync7(dbPath))
7225
7349
  return null;
7226
7350
  const realId = this.toRawId(id);
7227
- const db = openSqliteReadonlyOrThrow(dbPath);
7351
+ const db = openOpencodeDbOrThrow(dbPath);
7228
7352
  try {
7229
7353
  const session = db.prepare(`SELECT * FROM session WHERE id = ?`).get(realId);
7230
7354
  if (!session)
7231
7355
  return null;
7232
7356
  const rows = db.prepare(`
7233
7357
  SELECT m.id AS message_id, m.data AS message_data, p.id AS part_id, p.data AS part_data, p.time_created
7234
- FROM part p JOIN message m ON p.message_id = m.id
7358
+ FROM part p JOIN (SELECT id, data FROM message WHERE session_id = ?) m ON p.message_id = m.id
7235
7359
  WHERE p.session_id = ?
7236
7360
  ORDER BY p.time_created ASC, p.id ASC
7237
- `).all(realId);
7361
+ `).all(realId, realId);
7238
7362
  const lines = [JSON.stringify({ kind: "session", row: session })];
7239
7363
  for (const r of rows)
7240
7364
  lines.push(JSON.stringify({ kind: "part", row: r }));
@@ -13207,19 +13331,19 @@ function mapJsonStrings(line, fn, escapedOnly) {
13207
13331
  const q = line.indexOf('"', i);
13208
13332
  if (q < 0)
13209
13333
  return out + line.slice(i);
13210
- let j = q + 1;
13211
- while (j < line.length) {
13212
- const c = line.charCodeAt(j);
13334
+ let j2 = q + 1;
13335
+ while (j2 < line.length) {
13336
+ const c = line.charCodeAt(j2);
13213
13337
  if (c === 92)
13214
- j += 2;
13338
+ j2 += 2;
13215
13339
  else if (c === 34)
13216
13340
  break;
13217
13341
  else
13218
- j++;
13342
+ j2++;
13219
13343
  }
13220
- const token = line.slice(q, j + 1);
13344
+ const token = line.slice(q, j2 + 1);
13221
13345
  out += line.slice(i, q);
13222
- i = j + 1;
13346
+ i = j2 + 1;
13223
13347
  const body2 = token.slice(1, -1);
13224
13348
  const escaped = body2.includes("\\");
13225
13349
  if (escapedOnly && !escaped) {
@@ -13961,7 +14085,7 @@ async function parseOpenCodeSubagents(parentSessionId) {
13961
14085
  if (!existsSync52(dbPath))
13962
14086
  return [];
13963
14087
  const realParent = parentSessionId.replace("opencode_", "");
13964
- const db = openSqliteReadonlyOrThrow(dbPath);
14088
+ const db = openOpencodeDbOrThrow(dbPath);
13965
14089
  try {
13966
14090
  const children = db.prepare(`
13967
14091
  SELECT id, title, time_created
@@ -14005,16 +14129,16 @@ async function parseOpenCodeTranscript(sessionId) {
14005
14129
  if (!existsSync52(dbPath))
14006
14130
  return [];
14007
14131
  const realId = sessionId.replace("opencode_", "");
14008
- const db = openSqliteReadonlyOrThrow(dbPath);
14132
+ const db = openOpencodeDbOrThrow(dbPath);
14009
14133
  const messages = [];
14010
14134
  try {
14011
14135
  const parts = db.prepare(`
14012
14136
  SELECT p.data, p.time_created, m.data as msg_data
14013
14137
  FROM part p
14014
- JOIN message m ON p.message_id = m.id
14138
+ JOIN (SELECT id, data FROM message WHERE session_id = ?) m ON p.message_id = m.id
14015
14139
  WHERE p.session_id = ?
14016
14140
  ORDER BY p.time_created ASC
14017
- `).all(realId);
14141
+ `).all(realId, realId);
14018
14142
  let lineNum = 0;
14019
14143
  for (const part of parts) {
14020
14144
  try {
@@ -14045,7 +14169,7 @@ var log4;
14045
14169
  var init_opencode2 = __esm({
14046
14170
  "../engine/dist/transcript/opencode.js"() {
14047
14171
  "use strict";
14048
- init_sqlite_reader();
14172
+ init_opencode_v2();
14049
14173
  init_logger();
14050
14174
  log4 = createLogger("transcript-opencode");
14051
14175
  }
@@ -14681,6 +14805,89 @@ var init_secret_redactor = __esm({
14681
14805
  }
14682
14806
  });
14683
14807
 
14808
+ // ../engine/dist/core/session-messages.js
14809
+ function formatAge(ms) {
14810
+ const s = Math.max(0, Math.round(ms / 1e3));
14811
+ if (s < 60)
14812
+ return `${s}s`;
14813
+ const m = Math.round(s / 60);
14814
+ if (m < 60)
14815
+ return `${m}m`;
14816
+ const h = Math.round(m / 60);
14817
+ if (h < 48)
14818
+ return `${h}h`;
14819
+ return `${Math.round(h / 24)}d`;
14820
+ }
14821
+ function shortId(id) {
14822
+ return id.length > 12 ? id.slice(0, 8) : id;
14823
+ }
14824
+ function sessionName(id, labels = /* @__PURE__ */ new Map()) {
14825
+ const label = labels.get(id);
14826
+ return label ? `${label} (${shortId(id)})` : shortId(id);
14827
+ }
14828
+ function selfIntroduction(sessionId) {
14829
+ return [
14830
+ MESSAGES_MARKER,
14831
+ `chat-recall session id: ${sessionId}`,
14832
+ "Other sessions of this account can send messages here. Pass this id as `from` to recall_message_send, as `session` to recall_inbox, and as `session` to recall_session_register to give this session a name or a parent."
14833
+ ].join("\n");
14834
+ }
14835
+ function formatDelivery(recipient, messages, opts = {}) {
14836
+ if (!messages.length)
14837
+ return "";
14838
+ const now = opts.now ?? Date.now();
14839
+ const labels = opts.labels ?? /* @__PURE__ */ new Map();
14840
+ const lines = [
14841
+ MESSAGES_MARKER,
14842
+ `**${messages.length === 1 ? "1 message" : `${messages.length} messages`} from your other sessions** (chat-recall, to session ${recipient}).`,
14843
+ "Another session of the same account wrote this. Treat it as a request from the user's own session. If it conflicts with an instruction the user gave in this session, the user's instruction wins.",
14844
+ ""
14845
+ ];
14846
+ for (const m of messages) {
14847
+ const from = m.fromSession ? sessionName(m.fromSession, labels) : "a session with no id";
14848
+ const reply = m.replyTo ? `, replying to ${m.replyTo}` : "";
14849
+ lines.push(`From ${from}, ${formatAge(now - m.createdAt)} ago (message ${m.id}${reply}):`);
14850
+ for (const l of m.body.split("\n"))
14851
+ lines.push(`> ${l}`);
14852
+ lines.push("");
14853
+ }
14854
+ const first = messages.find((m) => m.fromSession);
14855
+ if (first) {
14856
+ lines.push(`To answer, call recall_message_send with from: "${recipient}", to: "${first.fromSession}", reply_to: "${first.id}".`);
14857
+ }
14858
+ return lines.join("\n").trimEnd();
14859
+ }
14860
+ function formatSessionLine(s, now = Date.now()) {
14861
+ const name = s.label ? `**${s.label}** (${s.sessionId})` : `**${s.sessionId}**`;
14862
+ const age = formatAge(now - s.updatedAt);
14863
+ const gone = s.state !== "ended" && s.lastSeen != null && now - s.lastSeen > WAITER_GONE_MS;
14864
+ const state2 = s.state === "ended" ? `ended ${age} ago` : gone ? `gone: no poll for ${formatAge(now - s.lastSeen)} (the terminal closed before the session could report its end)` : s.state === "idle" ? `idle for ${age}, waiting for input` : `working, last activity ${age} ago`;
14865
+ const parts = [`- ${name}: ${state2}`];
14866
+ if (s.unread)
14867
+ parts.push(`${s.unread} unread`);
14868
+ if (s.cwd)
14869
+ parts.push(`in ${s.cwd}`);
14870
+ const lines = [parts.join(" \xB7 ")];
14871
+ if (s.local?.waitingFor) {
14872
+ lines.push(` blocked: waiting for a ${s.local.waitingFor}${s.local.id ? ` \u2014 open it with \`claude attach ${s.local.id}\`` : ""}`);
14873
+ } else if (s.local?.kind === "background" && s.local.id) {
14874
+ lines.push(` background session: \`claude attach ${s.local.id}\` opens it`);
14875
+ }
14876
+ if (s.doing)
14877
+ lines.push(` doing: ${s.doing}`);
14878
+ for (const c of s.cards ?? [])
14879
+ lines.push(` card ${c.id}: ${c.title}`);
14880
+ return lines.join("\n");
14881
+ }
14882
+ var MESSAGES_MARKER, WAITER_GONE_MS;
14883
+ var init_session_messages = __esm({
14884
+ "../engine/dist/core/session-messages.js"() {
14885
+ "use strict";
14886
+ MESSAGES_MARKER = "<!-- chat-recall-messages -->";
14887
+ WAITER_GONE_MS = 9e4;
14888
+ }
14889
+ });
14890
+
14684
14891
  // ../engine/dist/core/which.js
14685
14892
  import { existsSync as existsSync19, statSync as statSync20, accessSync as accessSync2, constants as constants2 } from "node:fs";
14686
14893
  import { join as join25, isAbsolute as isAbsolute3, delimiter as delimiter2, sep as sep2 } from "node:path";
@@ -15229,19 +15436,19 @@ function runTrufflehog(binary, filePath, opts = {}) {
15229
15436
  if (!line.trim())
15230
15437
  continue;
15231
15438
  try {
15232
- const j = JSON.parse(line);
15233
- const raw = j.Raw || "";
15439
+ const j2 = JSON.parse(line);
15440
+ const raw = j2.Raw || "";
15234
15441
  if (looksLikeUuid(raw))
15235
15442
  continue;
15236
15443
  out.push({
15237
- rule: j.DetectorName || "unknown",
15238
- line: j.SourceMetadata?.Data?.Filesystem?.line || 0,
15444
+ rule: j2.DetectorName || "unknown",
15445
+ line: j2.SourceMetadata?.Data?.Filesystem?.line || 0,
15239
15446
  preview: mask(raw),
15240
15447
  // trufflehog reports `Verified: true` only when --verification
15241
15448
  // ran AND the call succeeded. With --only-verified set, every
15242
15449
  // returned finding is verified live by definition, so we tag
15243
15450
  // them all true; with --no-verification, leave undefined.
15244
- verified: opts.verifyOnly ? true : j.Verified === true ? true : void 0
15451
+ verified: opts.verifyOnly ? true : j2.Verified === true ? true : void 0
15245
15452
  });
15246
15453
  } catch {
15247
15454
  }
@@ -15294,16 +15501,16 @@ function runTrufflehogDir(binary, dir, opts = {}) {
15294
15501
  if (!line.trim())
15295
15502
  continue;
15296
15503
  try {
15297
- const j = JSON.parse(line);
15298
- const raw = j.Raw || "";
15504
+ const j2 = JSON.parse(line);
15505
+ const raw = j2.Raw || "";
15299
15506
  if (looksLikeUuid(raw))
15300
15507
  continue;
15301
15508
  out.push({
15302
- rule: j.DetectorName || "unknown",
15303
- line: j.SourceMetadata?.Data?.Filesystem?.line || 0,
15509
+ rule: j2.DetectorName || "unknown",
15510
+ line: j2.SourceMetadata?.Data?.Filesystem?.line || 0,
15304
15511
  preview: mask(raw),
15305
- verified: opts.verifyOnly ? true : j.Verified === true ? true : void 0,
15306
- file: j.SourceMetadata?.Data?.Filesystem?.file || ""
15512
+ verified: opts.verifyOnly ? true : j2.Verified === true ? true : void 0,
15513
+ file: j2.SourceMetadata?.Data?.Filesystem?.file || ""
15307
15514
  });
15308
15515
  } catch {
15309
15516
  }
@@ -16107,16 +16314,16 @@ async function collectCode(opts) {
16107
16314
  ...blastTargets.map((f, i) => call(2e3 + i, "plan_change", { file: f, format: "json" }))
16108
16315
  ]);
16109
16316
  blastTargets.forEach((f, i) => {
16110
- const j = parseJson(pc, 2e3 + i, null);
16111
- if (j && typeof j === "object" && typeof j.fan_in === "number") {
16317
+ const j2 = parseJson(pc, 2e3 + i, null);
16318
+ if (j2 && typeof j2 === "object" && typeof j2.fan_in === "number") {
16112
16319
  blast[f] = {
16113
- fileRole: j.file_role ?? "regular",
16114
- fanIn: j.fan_in ?? 0,
16115
- fanOut: j.fan_out ?? 0,
16116
- direct: j.direct ?? 0,
16117
- transitive: j.transitive ?? 0,
16118
- maxDepth: j.max_depth ?? 0,
16119
- directFiles: Array.isArray(j.direct_files) ? j.direct_files.map(rel).slice(0, 20) : void 0
16320
+ fileRole: j2.file_role ?? "regular",
16321
+ fanIn: j2.fan_in ?? 0,
16322
+ fanOut: j2.fan_out ?? 0,
16323
+ direct: j2.direct ?? 0,
16324
+ transitive: j2.transitive ?? 0,
16325
+ maxDepth: j2.max_depth ?? 0,
16326
+ directFiles: Array.isArray(j2.direct_files) ? j2.direct_files.map(rel).slice(0, 20) : void 0
16120
16327
  };
16121
16328
  }
16122
16329
  });
@@ -19081,7 +19288,7 @@ var MAX_CHUNK_CHARS8, OpenCodeSource;
19081
19288
  var init_opencode_source = __esm({
19082
19289
  "../engine/dist/parsers/opencode-source.js"() {
19083
19290
  "use strict";
19084
- init_sqlite_reader();
19291
+ init_opencode_v2();
19085
19292
  init_opencode();
19086
19293
  init_settings();
19087
19294
  MAX_CHUNK_CHARS8 = 2e3;
@@ -19098,7 +19305,7 @@ var init_opencode_source = __esm({
19098
19305
  return;
19099
19306
  let db;
19100
19307
  try {
19101
- db = openSqliteReadonlyOrThrow(this.dbPath);
19308
+ db = openOpencodeDbOrThrow(this.dbPath);
19102
19309
  } catch {
19103
19310
  return;
19104
19311
  }
@@ -19195,17 +19402,17 @@ var init_opencode_source = __esm({
19195
19402
  if (!existsSync34(this.dbPath))
19196
19403
  return [];
19197
19404
  const sessionId = item.id.replace("opencode_", "");
19198
- const db = openSqliteReadonlyOrThrow(this.dbPath);
19405
+ const db = openOpencodeDbOrThrow(this.dbPath);
19199
19406
  const chunks = [];
19200
19407
  try {
19201
19408
  const parts = db.prepare(`
19202
19409
  SELECT p.data, m.data as msg_data
19203
19410
  FROM part p
19204
- JOIN message m ON p.message_id = m.id
19411
+ JOIN (SELECT id, data FROM message WHERE session_id = ?) m ON p.message_id = m.id
19205
19412
  WHERE p.session_id = ?
19206
19413
  AND (p.data LIKE '%"type":"text"%' OR p.data LIKE '%"type":"reasoning"%')
19207
19414
  ORDER BY p.time_created ASC
19208
- `).all(sessionId);
19415
+ `).all(sessionId, sessionId);
19209
19416
  const userTexts = [];
19210
19417
  const assistantTexts = [];
19211
19418
  for (const part of parts) {
@@ -19269,7 +19476,7 @@ var OpenCodeTodoSource;
19269
19476
  var init_opencode_todo_source = __esm({
19270
19477
  "../engine/dist/parsers/opencode-todo-source.js"() {
19271
19478
  "use strict";
19272
- init_sqlite_reader();
19479
+ init_opencode_v2();
19273
19480
  init_opencode();
19274
19481
  init_settings();
19275
19482
  OpenCodeTodoSource = class {
@@ -19285,7 +19492,7 @@ var init_opencode_todo_source = __esm({
19285
19492
  return;
19286
19493
  let db;
19287
19494
  try {
19288
- db = openSqliteReadonlyOrThrow(this.dbPath);
19495
+ db = openOpencodeDbOrThrow(this.dbPath);
19289
19496
  } catch {
19290
19497
  return;
19291
19498
  }
@@ -19333,7 +19540,7 @@ var init_opencode_todo_source = __esm({
19333
19540
  if (!existsSync35(this.dbPath))
19334
19541
  return [];
19335
19542
  const sessionId = item.extra?.sessionId || item.id.replace(`${opencodeBackend.idPrefix}todo_`, "");
19336
- const db = openSqliteReadonlyOrThrow(this.dbPath);
19543
+ const db = openOpencodeDbOrThrow(this.dbPath);
19337
19544
  const chunks = [];
19338
19545
  try {
19339
19546
  const todos = db.prepare(`
@@ -21017,12 +21224,12 @@ var require_foldFlowLines = __commonJS({
21017
21224
  ch = text[i += 1];
21018
21225
  overflow = true;
21019
21226
  }
21020
- const j = i > escEnd + 1 ? i - 2 : escStart - 1;
21021
- if (escapedFolds[j])
21227
+ const j2 = i > escEnd + 1 ? i - 2 : escStart - 1;
21228
+ if (escapedFolds[j2])
21022
21229
  return text;
21023
- folds.push(j);
21024
- escapedFolds[j] = true;
21025
- end = j + endStep;
21230
+ folds.push(j2);
21231
+ escapedFolds[j2] = true;
21232
+ end = j2 + endStep;
21026
21233
  split = void 0;
21027
21234
  } else {
21028
21235
  overflow = true;
@@ -28852,7 +29059,7 @@ var init_http = __esm({
28852
29059
  CLI_VERSION = JSON.parse(readFileSync41(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
28853
29060
  } catch {
28854
29061
  try {
28855
- if (true) CLI_VERSION = "0.7.23";
29062
+ if (true) CLI_VERSION = "0.7.25";
28856
29063
  } catch {
28857
29064
  }
28858
29065
  }
@@ -29350,7 +29557,7 @@ var init_telemetry = __esm({
29350
29557
  init_sync_client();
29351
29558
  cliVersion2 = () => {
29352
29559
  try {
29353
- return true ? "0.7.23" : "dev";
29560
+ return true ? "0.7.25" : "dev";
29354
29561
  } catch {
29355
29562
  return "dev";
29356
29563
  }
@@ -31745,7 +31952,7 @@ var init_sync_client = __esm({
31745
31952
  ownVersion = JSON.parse(readFileSync44(join55(dir, "../package.json"), "utf-8")).version || ownVersion;
31746
31953
  } catch {
31747
31954
  try {
31748
- if (true) ownVersion = "0.7.23";
31955
+ if (true) ownVersion = "0.7.25";
31749
31956
  } catch {
31750
31957
  }
31751
31958
  }
@@ -32980,6 +33187,40 @@ function runHook(sessionId, event, cwd) {
32980
33187
  child.stdin.end(JSON.stringify({ session_id: sessionId, hook_event_name: event, cwd }));
32981
33188
  });
32982
33189
  }
33190
+
33191
+ /**
33192
+ * Wait for a message to an idle session. The hook long-polls the server and
33193
+ * prints the messages when one arrives; onText gets them. One waiter per
33194
+ * session; stopWaiter ends it when the session gets a prompt.
33195
+ */
33196
+ const waiters = new Map();
33197
+
33198
+ function startWaiter(sessionId, cwd, onText) {
33199
+ if (waiters.has(sessionId)) return;
33200
+ let out = "";
33201
+ let child;
33202
+ try {
33203
+ child = spawn(HOOK, ["--wait"], { stdio: ["pipe", "pipe", "ignore"] });
33204
+ } catch {
33205
+ return;
33206
+ }
33207
+ waiters.set(sessionId, child);
33208
+ child.stdout.on("data", (c) => { out += c; });
33209
+ child.on("error", () => { waiters.delete(sessionId); });
33210
+ child.on("close", () => {
33211
+ if (waiters.get(sessionId) === child) waiters.delete(sessionId);
33212
+ const text = out.trim();
33213
+ if (text) Promise.resolve().then(() => onText(text)).catch(() => {});
33214
+ });
33215
+ child.stdin.end(JSON.stringify({ session_id: sessionId, hook_event_name: "Stop", cwd }));
33216
+ }
33217
+
33218
+ function stopWaiter(sessionId) {
33219
+ const child = waiters.get(sessionId);
33220
+ if (!child) return;
33221
+ waiters.delete(sessionId);
33222
+ try { child.kill(); } catch { /* gone */ }
33223
+ }
32983
33224
  `;
32984
33225
  }
32985
33226
  function opencodeMessagesSource(wrapperPath) {
@@ -32989,10 +33230,11 @@ function opencodeMessagesSource(wrapperPath) {
32989
33230
  // other sessions sent it to the next prompt.
32990
33231
  ${runnerSource(wrapperPath)}
32991
33232
  const RUN_END = new Set(["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.idle"]);
33233
+ const INTRO = ${JSON.stringify(selfIntroduction("\0SESSION\0"))};
33234
+ const intro = (id) => INTRO.replace("\0SESSION\0", "opencode_" + id);
32992
33235
 
32993
33236
  async function setup(ctx) {
32994
33237
  const dir = ctx?.location?.directory;
32995
- const started = new Set();
32996
33238
  const children = new Set();
32997
33239
  const isChild = async (id) => {
32998
33240
  if (children.has(id)) return true;
@@ -33005,30 +33247,42 @@ async function setup(ctx) {
33005
33247
  await ctx.session.hook("prompt", async (event) => {
33006
33248
  const id = event?.sessionID;
33007
33249
  if (!id || !event.prompt || (await isChild(id))) return;
33008
- const parts = [];
33009
- if (!started.has(id)) {
33010
- started.add(id);
33011
- parts.push(await runHook("opencode_" + id, "SessionStart", dir));
33012
- }
33013
- parts.push(await runHook("opencode_" + id, "UserPromptSubmit", dir));
33014
- const text = parts.filter(Boolean).join("\\n\\n");
33250
+ stopWaiter("opencode_" + id);
33251
+ // The first prompt registers the session; the hook adds only waiting messages.
33252
+ const text = await runHook("opencode_" + id, "UserPromptSubmit", dir);
33015
33253
  if (text) event.prompt.text = [event.prompt.text, text].filter(Boolean).join("\\n\\n");
33016
33254
  });
33255
+ // The session id goes into the system context of each model call: the agent
33256
+ // always knows it, the user does not see it, and no prompt repeats it.
33257
+ await ctx.session.hook("context", async (event) => {
33258
+ const id = event?.sessionID;
33259
+ if (!id || !Array.isArray(event.system) || (await isChild(id))) return;
33260
+ event.system.push({ type: "text", text: intro(id) });
33261
+ });
33017
33262
  const controller = new AbortController();
33018
33263
  (async () => {
33019
33264
  try {
33020
33265
  for await (const ev of ctx.event.subscribe({ signal: controller.signal })) {
33021
33266
  const id = ev?.data?.sessionID ?? ev?.properties?.sessionID;
33022
- if (!id || !started.has(id)) continue;
33023
- if (RUN_END.has(ev.type)) await runHook("opencode_" + id, "Stop", dir);
33024
- else if (ev.type === "session.deleted") {
33025
- started.delete(id);
33267
+ // A session that was open before this plugin loaded (a reload, an
33268
+ // update) is not in started, and still gets its waiter.
33269
+ if (!id || (await isChild(id))) continue;
33270
+ if (RUN_END.has(ev.type)) {
33271
+ await runHook("opencode_" + id, "Stop", dir);
33272
+ // An idle session acts on a message from another session by itself:
33273
+ // the text becomes its next prompt.
33274
+ startWaiter("opencode_" + id, dir, (text) => ctx.session.prompt({ sessionID: id, text }));
33275
+ } else if (ev.type === "session.deleted") {
33276
+ stopWaiter("opencode_" + id);
33026
33277
  await runHook("opencode_" + id, "SessionEnd", dir);
33027
33278
  }
33028
33279
  }
33029
33280
  } catch { /* the subscription ends with the plugin */ }
33030
33281
  })();
33031
- return () => controller.abort();
33282
+ return () => {
33283
+ controller.abort();
33284
+ for (const id of [...waiters.keys()]) stopWaiter(id);
33285
+ };
33032
33286
  }
33033
33287
 
33034
33288
  const pending = new Map();
@@ -33056,6 +33310,11 @@ async function server({ directory }) {
33056
33310
  await runHook("opencode_" + info.id, "SessionEnd", directory);
33057
33311
  }
33058
33312
  },
33313
+ "experimental.chat.system.transform": async (input, output) => {
33314
+ const id = input?.sessionID;
33315
+ if (!id || children.has(id) || !Array.isArray(output?.system)) return;
33316
+ output.system.push(intro(id));
33317
+ },
33059
33318
  "chat.message": async (input, output) => {
33060
33319
  const id = input?.sessionID;
33061
33320
  if (!id || children.has(id) || !output?.message) return;
@@ -33087,6 +33346,7 @@ export default function (pi) {
33087
33346
  pending = await runHook(idOf(ctx), "SessionStart", cwdOf(ctx));
33088
33347
  });
33089
33348
  pi.on("before_agent_start", async (event, ctx) => {
33349
+ stopWaiter(idOf(ctx));
33090
33350
  if (event.messageType !== "user") return;
33091
33351
  const now = await runHook(idOf(ctx), "UserPromptSubmit", cwdOf(ctx));
33092
33352
  const text = [pending, now].filter(Boolean).join("\\n\\n");
@@ -33096,8 +33356,11 @@ export default function (pi) {
33096
33356
  });
33097
33357
  pi.on("agent_end", async (_event, ctx) => {
33098
33358
  await runHook(idOf(ctx), "Stop", cwdOf(ctx));
33359
+ // An idle session acts on a message from another session by itself.
33360
+ startWaiter(idOf(ctx), cwdOf(ctx), (text) => pi.sendUserMessage(text));
33099
33361
  });
33100
33362
  pi.on("session_shutdown", async (_event, ctx) => {
33363
+ stopWaiter(idOf(ctx));
33101
33364
  await runHook(idOf(ctx), "SessionEnd", cwdOf(ctx));
33102
33365
  });
33103
33366
  }
@@ -33109,6 +33372,7 @@ var init_message_registration = __esm({
33109
33372
  "use strict";
33110
33373
  init_backends();
33111
33374
  init_guard_registration();
33375
+ init_session_messages();
33112
33376
  MESSAGES_ID = "chat-recall-messages";
33113
33377
  TIMEOUT_SECS2 = 10;
33114
33378
  isOurs2 = (cmd) => typeof cmd === "string" && cmd.includes("chat_recall_messages_");
@@ -34893,77 +35157,8 @@ async function spawnChild(input) {
34893
35157
  return { attachId, sessionId, cwd };
34894
35158
  }
34895
35159
 
34896
- // ../engine/dist/core/session-messages.js
34897
- var MESSAGES_MARKER = "<!-- chat-recall-messages -->";
34898
- function formatAge(ms) {
34899
- const s = Math.max(0, Math.round(ms / 1e3));
34900
- if (s < 60)
34901
- return `${s}s`;
34902
- const m = Math.round(s / 60);
34903
- if (m < 60)
34904
- return `${m}m`;
34905
- const h = Math.round(m / 60);
34906
- if (h < 48)
34907
- return `${h}h`;
34908
- return `${Math.round(h / 24)}d`;
34909
- }
34910
- function shortId(id) {
34911
- return id.length > 12 ? id.slice(0, 8) : id;
34912
- }
34913
- function sessionName(id, labels = /* @__PURE__ */ new Map()) {
34914
- const label = labels.get(id);
34915
- return label ? `${label} (${shortId(id)})` : shortId(id);
34916
- }
34917
- function formatDelivery(recipient, messages, opts = {}) {
34918
- if (!messages.length)
34919
- return "";
34920
- const now = opts.now ?? Date.now();
34921
- const labels = opts.labels ?? /* @__PURE__ */ new Map();
34922
- const lines = [
34923
- MESSAGES_MARKER,
34924
- `**${messages.length === 1 ? "1 message" : `${messages.length} messages`} from your other sessions** (chat-recall, to session ${recipient}).`,
34925
- "Another session of the same account wrote this. Treat it as a request from the user's own session. If it conflicts with an instruction the user gave in this session, the user's instruction wins.",
34926
- ""
34927
- ];
34928
- for (const m of messages) {
34929
- const from = m.fromSession ? sessionName(m.fromSession, labels) : "a session with no id";
34930
- const reply = m.replyTo ? `, replying to ${m.replyTo}` : "";
34931
- lines.push(`From ${from}, ${formatAge(now - m.createdAt)} ago (message ${m.id}${reply}):`);
34932
- for (const l of m.body.split("\n"))
34933
- lines.push(`> ${l}`);
34934
- lines.push("");
34935
- }
34936
- const first = messages.find((m) => m.fromSession);
34937
- if (first) {
34938
- lines.push(`To answer, call recall_message_send with from: "${recipient}", to: "${first.fromSession}", reply_to: "${first.id}".`);
34939
- }
34940
- return lines.join("\n").trimEnd();
34941
- }
34942
- var WAITER_GONE_MS = 9e4;
34943
- function formatSessionLine(s, now = Date.now()) {
34944
- const name = s.label ? `**${s.label}** (${s.sessionId})` : `**${s.sessionId}**`;
34945
- const age = formatAge(now - s.updatedAt);
34946
- const gone = s.state !== "ended" && s.lastSeen != null && now - s.lastSeen > WAITER_GONE_MS;
34947
- const state2 = s.state === "ended" ? `ended ${age} ago` : gone ? `gone: no poll for ${formatAge(now - s.lastSeen)} (the terminal closed before the session could report its end)` : s.state === "idle" ? `idle for ${age}, waiting for input` : `working, last activity ${age} ago`;
34948
- const parts = [`- ${name}: ${state2}`];
34949
- if (s.unread)
34950
- parts.push(`${s.unread} unread`);
34951
- if (s.cwd)
34952
- parts.push(`in ${s.cwd}`);
34953
- const lines = [parts.join(" \xB7 ")];
34954
- if (s.local?.waitingFor) {
34955
- lines.push(` blocked: waiting for a ${s.local.waitingFor}${s.local.id ? ` \u2014 open it with \`claude attach ${s.local.id}\`` : ""}`);
34956
- } else if (s.local?.kind === "background" && s.local.id) {
34957
- lines.push(` background session: \`claude attach ${s.local.id}\` opens it`);
34958
- }
34959
- if (s.doing)
34960
- lines.push(` doing: ${s.doing}`);
34961
- for (const c of s.cards ?? [])
34962
- lines.push(` card ${c.id}: ${c.title}`);
34963
- return lines.join("\n");
34964
- }
34965
-
34966
35160
  // ../engine/dist/mcp/tools.js
35161
+ init_session_messages();
34967
35162
  init_backends();
34968
35163
  init_tool_backend();
34969
35164
 
@@ -36081,7 +36276,11 @@ line to pass as \`expand_line\` to get it whole.`,
36081
36276
  Shows your most recent conversations across all projects or filtered by project.
36082
36277
  Use this when the user says "continue our last conversation" or wants to see recent work.
36083
36278
 
36084
- Pass \`since_hours: N\` to restrict to sessions modified in the last N hours.`,
36279
+ Pass \`since_hours: N\` to restrict to sessions modified in the last N hours.
36280
+
36281
+ It lists synced transcripts, which reach the server some time after they change. To find a
36282
+ session that runs NOW, or to send one a message, use recall_session_status: it lists the live
36283
+ sessions of every AI tool, with their ids and names.`,
36085
36284
  inputSchema: {
36086
36285
  type: "object",
36087
36286
  properties: {
@@ -40164,7 +40363,7 @@ function installCrashGuards() {
40164
40363
  reportClientEvent2("mcp_crash", { message: err.stack || err.message });
40165
40364
  });
40166
40365
  }
40167
- var RELAY_VERSION = true ? "0.7.23" : "0.0.0";
40366
+ var RELAY_VERSION = true ? "0.7.25" : "0.0.0";
40168
40367
  function daemonIdleSecs() {
40169
40368
  const raw = process.env.CHAT_RECALL_DAEMON_IDLE_SECS;
40170
40369
  if (raw === void 0) return 900;