@shipers-dev/multi 0.49.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +410 -127
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -33133,25 +33133,146 @@ var init_workspace_mutex = __esm(() => {
33133
33133
  ]);
33134
33134
  });
33135
33135
 
33136
+ // src/_impl/adapter-pidfile.ts
33137
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
33138
+ import { homedir } from "os";
33139
+ import { join as join7 } from "path";
33140
+ import { spawnSync } from "child_process";
33141
+ function ensureDir() {
33142
+ if (!existsSync5(ADAPTERS_DIR))
33143
+ mkdirSync5(ADAPTERS_DIR, { recursive: true });
33144
+ }
33145
+ function pidfilePath(pid) {
33146
+ return join7(ADAPTERS_DIR, `${pid}.json`);
33147
+ }
33148
+ function writeAdapterPidfile(entry) {
33149
+ ensureDir();
33150
+ const full = {
33151
+ ...entry,
33152
+ daemon_pid: entry.daemon_pid ?? process.pid,
33153
+ started_at: Date.now()
33154
+ };
33155
+ const path = pidfilePath(entry.pid);
33156
+ try {
33157
+ writeFileSync5(path, JSON.stringify(full) + `
33158
+ `, "utf8");
33159
+ } catch {}
33160
+ return path;
33161
+ }
33162
+ function removeAdapterPidfile(pid) {
33163
+ try {
33164
+ unlinkSync3(pidfilePath(pid));
33165
+ } catch {}
33166
+ }
33167
+ function inspectPid(pid) {
33168
+ try {
33169
+ const r = spawnSync("ps", ["-o", "ppid=,command=", "-p", String(pid)], { encoding: "utf8" });
33170
+ if (r.status !== 0)
33171
+ return { alive: false, ppid: 0, command: "" };
33172
+ const line = (r.stdout || "").trim();
33173
+ if (!line)
33174
+ return { alive: false, ppid: 0, command: "" };
33175
+ const m = line.match(/^\s*(\d+)\s+(.*)$/);
33176
+ if (!m)
33177
+ return { alive: false, ppid: 0, command: "" };
33178
+ return { alive: true, ppid: parseInt(m[1], 10), command: m[2] };
33179
+ } catch {
33180
+ return { alive: false, ppid: 0, command: "" };
33181
+ }
33182
+ }
33183
+ function tryKill(pid, signal) {
33184
+ try {
33185
+ process.kill(pid, signal);
33186
+ return true;
33187
+ } catch {
33188
+ return false;
33189
+ }
33190
+ }
33191
+ function isPidAlive(pid) {
33192
+ try {
33193
+ process.kill(pid, 0);
33194
+ return true;
33195
+ } catch {
33196
+ return false;
33197
+ }
33198
+ }
33199
+ function reapStaleAdapters(log3 = () => {}) {
33200
+ ensureDir();
33201
+ let entries2 = [];
33202
+ try {
33203
+ entries2 = readdirSync2(ADAPTERS_DIR);
33204
+ } catch {
33205
+ return 0;
33206
+ }
33207
+ let reaped = 0;
33208
+ for (const name of entries2) {
33209
+ if (!name.endsWith(".json"))
33210
+ continue;
33211
+ const path = join7(ADAPTERS_DIR, name);
33212
+ let parsed = null;
33213
+ try {
33214
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
33215
+ } catch {}
33216
+ if (!parsed || typeof parsed.pid !== "number") {
33217
+ try {
33218
+ unlinkSync3(path);
33219
+ } catch {}
33220
+ continue;
33221
+ }
33222
+ const info = inspectPid(parsed.pid);
33223
+ if (!info.alive) {
33224
+ try {
33225
+ unlinkSync3(path);
33226
+ } catch {}
33227
+ continue;
33228
+ }
33229
+ if (parsed.daemon_pid && isPidAlive(parsed.daemon_pid))
33230
+ continue;
33231
+ if (parsed.bin && !info.command.includes(parsed.bin)) {
33232
+ try {
33233
+ unlinkSync3(path);
33234
+ } catch {}
33235
+ continue;
33236
+ }
33237
+ if (info.ppid !== 1)
33238
+ continue;
33239
+ log3(`[reap] killing orphan ${parsed.kind} pid=${parsed.pid} bin=${parsed.bin} session=${parsed.session_id ?? "?"}`);
33240
+ tryKill(parsed.pid, "SIGTERM");
33241
+ setTimeout(() => {
33242
+ if (isPidAlive(parsed.pid))
33243
+ tryKill(parsed.pid, "SIGKILL");
33244
+ }, 1500).unref?.();
33245
+ try {
33246
+ unlinkSync3(path);
33247
+ } catch {}
33248
+ reaped++;
33249
+ }
33250
+ return reaped;
33251
+ }
33252
+ var ADAPTERS_DIR;
33253
+ var init_adapter_pidfile = __esm(() => {
33254
+ ADAPTERS_DIR = join7(homedir(), ".multi", "adapters");
33255
+ });
33256
+
33136
33257
  // src/_impl/acp-runner.ts
33137
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync5 } from "fs";
33138
- import { dirname as dirname6, join as join7 } from "path";
33258
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync6 } from "fs";
33259
+ import { dirname as dirname6, join as join8 } from "path";
33139
33260
  function ensureBypassPermissions(cwd) {
33140
33261
  try {
33141
- const dir = join7(cwd, ".claude");
33142
- if (!existsSync5(dir))
33143
- mkdirSync5(dir, { recursive: true });
33144
- const path = join7(dir, "settings.local.json");
33262
+ const dir = join8(cwd, ".claude");
33263
+ if (!existsSync6(dir))
33264
+ mkdirSync6(dir, { recursive: true });
33265
+ const path = join8(dir, "settings.local.json");
33145
33266
  let json2 = {};
33146
- if (existsSync5(path)) {
33267
+ if (existsSync6(path)) {
33147
33268
  try {
33148
- json2 = JSON.parse(readFileSync5(path, "utf8"));
33269
+ json2 = JSON.parse(readFileSync6(path, "utf8"));
33149
33270
  } catch {
33150
33271
  json2 = {};
33151
33272
  }
33152
33273
  }
33153
33274
  json2.permissions = { ...json2.permissions || {}, defaultMode: "bypassPermissions" };
33154
- writeFileSync5(path, JSON.stringify(json2, null, 2) + `
33275
+ writeFileSync6(path, JSON.stringify(json2, null, 2) + `
33155
33276
  `, "utf8");
33156
33277
  } catch {}
33157
33278
  }
@@ -33195,6 +33316,16 @@ async function runAcp(opts) {
33195
33316
  try {
33196
33317
  opts.onSpawn?.(child);
33197
33318
  } catch {}
33319
+ const pidfileBin = argv[0]?.split("/").pop() || "claude-code-acp";
33320
+ if (typeof child.pid === "number") {
33321
+ writeAdapterPidfile({
33322
+ pid: child.pid,
33323
+ bin: pidfileBin,
33324
+ kind: "acp",
33325
+ session_id: opts.sessionId ?? null,
33326
+ issue_id: opts.issueId ?? null
33327
+ });
33328
+ }
33198
33329
  const output = new WritableStream({
33199
33330
  write(chunk2) {
33200
33331
  child.stdin.write(chunk2);
@@ -33224,7 +33355,7 @@ async function runAcp(opts) {
33224
33355
  },
33225
33356
  async readTextFile(params) {
33226
33357
  try {
33227
- const content = readFileSync5(params.path, "utf8");
33358
+ const content = readFileSync6(params.path, "utf8");
33228
33359
  const sliced = typeof params.line === "number" && typeof params.limit === "number" ? content.split(`
33229
33360
  `).slice(params.line, params.line + params.limit).join(`
33230
33361
  `) : content;
@@ -33236,9 +33367,9 @@ async function runAcp(opts) {
33236
33367
  async writeTextFile(params) {
33237
33368
  try {
33238
33369
  const dir = dirname6(params.path);
33239
- if (!existsSync5(dir))
33240
- mkdirSync5(dir, { recursive: true });
33241
- writeFileSync5(params.path, params.content, "utf8");
33370
+ if (!existsSync6(dir))
33371
+ mkdirSync6(dir, { recursive: true });
33372
+ writeFileSync6(params.path, params.content, "utf8");
33242
33373
  return {};
33243
33374
  } catch (e) {
33244
33375
  throw new Error(`writeTextFile failed: ${fmtErr(e)}`);
@@ -33327,6 +33458,8 @@ async function runAcp(opts) {
33327
33458
  try {
33328
33459
  child.kill();
33329
33460
  } catch {}
33461
+ if (typeof child.pid === "number")
33462
+ removeAdapterPidfile(child.pid);
33330
33463
  }
33331
33464
  async function handleSessionUpdate(params, o) {
33332
33465
  const u = params.update;
@@ -33479,11 +33612,12 @@ var init_acp_runner = __esm(() => {
33479
33612
  init_acp();
33480
33613
  init_client();
33481
33614
  init_workspace_mutex();
33615
+ init_adapter_pidfile();
33482
33616
  });
33483
33617
 
33484
33618
  // src/_impl/acpx-runner.ts
33485
33619
  import { appendFileSync as appendFileSync3 } from "fs";
33486
- import { join as join8 } from "path";
33620
+ import { join as join9 } from "path";
33487
33621
  function dlog(msg) {
33488
33622
  try {
33489
33623
  appendFileSync3(LOG_PATH2, `[${new Date().toISOString()}] ${msg}
@@ -33502,6 +33636,9 @@ async function spawnAcpxPrompt(agentType, cwd, sessionName, prompt, collectAssis
33502
33636
  args2.push(prompt);
33503
33637
  dlog(`[acpx] prompt: ${args2.slice(0, 10).join(" ")} ... (prompt len=${prompt.length})`);
33504
33638
  const proc = Bun.spawn(args2, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
33639
+ if (typeof proc.pid === "number") {
33640
+ writeAdapterPidfile({ pid: proc.pid, bin: "acpx", kind: "acpx", session_id: sessionName ?? null });
33641
+ }
33505
33642
  let stopReason = "end_turn";
33506
33643
  (async () => {
33507
33644
  try {
@@ -33570,6 +33707,8 @@ async function spawnAcpxPrompt(agentType, cwd, sessionName, prompt, collectAssis
33570
33707
  }
33571
33708
  }
33572
33709
  const code = await proc.exited;
33710
+ if (typeof proc.pid === "number")
33711
+ removeAdapterPidfile(proc.pid);
33573
33712
  if (code !== 0 && code !== 130) {
33574
33713
  if (!collectAssistantText)
33575
33714
  await forward({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
@@ -33605,6 +33744,9 @@ async function runAcpx(opts) {
33605
33744
  try {
33606
33745
  opts.onSpawn?.(proc);
33607
33746
  } catch {}
33747
+ if (typeof proc.pid === "number") {
33748
+ writeAdapterPidfile({ pid: proc.pid, bin: "acpx", kind: "acpx", session_id: opts.sessionName ?? null });
33749
+ }
33608
33750
  let stopReason = "end_turn";
33609
33751
  (async () => {
33610
33752
  try {
@@ -33661,6 +33803,8 @@ async function runAcpx(opts) {
33661
33803
  await opts.onEvent(ev);
33662
33804
  }
33663
33805
  const code = await proc.exited;
33806
+ if (typeof proc.pid === "number")
33807
+ removeAdapterPidfile(proc.pid);
33664
33808
  if (code !== 0 && code !== 130) {
33665
33809
  await opts.onEvent({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
33666
33810
  }
@@ -33776,8 +33920,9 @@ function extractText2(content) {
33776
33920
  }
33777
33921
  var HOME3, LOG_PATH2;
33778
33922
  var init_acpx_runner = __esm(() => {
33923
+ init_adapter_pidfile();
33779
33924
  HOME3 = process.env.HOME || process.env.USERPROFILE || ".";
33780
- LOG_PATH2 = join8(HOME3, ".multi", "logs", "agent.log");
33925
+ LOG_PATH2 = join9(HOME3, ".multi", "logs", "agent.log");
33781
33926
  });
33782
33927
 
33783
33928
  // ../../node_modules/zod/index.js
@@ -34003,12 +34148,12 @@ function parsePlanBlocks(text) {
34003
34148
  }
34004
34149
  return { actions, errors: errors3 };
34005
34150
  }
34006
- var PLAN_SCHEMA_VERSION = 5, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34151
+ var PLAN_SCHEMA_VERSION = 7, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34007
34152
  var init_plans = __esm(() => {
34008
34153
  init_zod();
34009
34154
  Priority = exports_external.enum(["low", "medium", "high"]);
34010
34155
  AssigneeType = exports_external.enum(["human", "agent"]);
34011
- IssueStatus = exports_external.enum(["todo", "in_progress", "done", "failed", "stopped", "cancelled"]);
34156
+ IssueStatus = exports_external.enum(["todo", "in_progress", "blocked", "done", "archived", "failed", "stopped", "cancelled"]);
34012
34157
  SessionRole = exports_external.enum(["implementer", "reviewer", "test-fixer"]);
34013
34158
  SkillFile = exports_external.object({ path: exports_external.string().min(1), content: exports_external.string() });
34014
34159
  EvalPolicy = exports_external.object({
@@ -34123,6 +34268,20 @@ var init_plans = __esm(() => {
34123
34268
  score: exports_external.number().min(0).max(1),
34124
34269
  feedback: exports_external.string().min(1).max(8000),
34125
34270
  scores: exports_external.record(exports_external.string(), exports_external.number().min(0).max(1)).optional()
34271
+ }),
34272
+ exports_external.object({
34273
+ type: exports_external.literal("memory.search"),
34274
+ project_id: exports_external.string().optional(),
34275
+ query: exports_external.string().min(1).max(1000),
34276
+ limit: exports_external.number().int().min(1).max(50).optional()
34277
+ }),
34278
+ exports_external.object({
34279
+ type: exports_external.literal("memory.write"),
34280
+ project_id: exports_external.string().optional(),
34281
+ text: exports_external.string().min(1).max(20000),
34282
+ summary: exports_external.string().max(2000).optional(),
34283
+ kind: exports_external.string().min(1).max(64).optional(),
34284
+ source_id: exports_external.string().min(1).max(256).optional()
34126
34285
  })
34127
34286
  ]);
34128
34287
  PlanEnvelopeSchema = exports_external.object({
@@ -34220,8 +34379,8 @@ var init_lib = __esm(() => {
34220
34379
 
34221
34380
  // src/_impl/git-enforce.ts
34222
34381
  import { spawn as spawn2 } from "node:child_process";
34223
- import { existsSync as existsSync9 } from "node:fs";
34224
- import { join as join9 } from "node:path";
34382
+ import { existsSync as existsSync10 } from "node:fs";
34383
+ import { join as join10 } from "node:path";
34225
34384
  function run4(cwd, cmd, args2) {
34226
34385
  return new Promise((resolve2) => {
34227
34386
  const p = spawn2(cmd, args2, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -34241,7 +34400,7 @@ function normalizeKey2(issueKey) {
34241
34400
  return issueKey.toLowerCase().replace(/[^a-z0-9\-_\/]/g, "-");
34242
34401
  }
34243
34402
  function worktreePath(baseWorkingDir, issueKey) {
34244
- return join9(baseWorkingDir, ".multi", "worktrees", normalizeKey2(issueKey));
34403
+ return join10(baseWorkingDir, ".multi", "worktrees", normalizeKey2(issueKey));
34245
34404
  }
34246
34405
  function branchFor(issueKey) {
34247
34406
  return `multi/${normalizeKey2(issueKey)}`;
@@ -34260,7 +34419,7 @@ function enforceCommitAndPush(baseWorkingDir, issueKey, mode = "enforce") {
34260
34419
  };
34261
34420
  if (mode === "off")
34262
34421
  return result;
34263
- if (!existsSync9(wt))
34422
+ if (!existsSync10(wt))
34264
34423
  return result;
34265
34424
  if (!(yield* isGitRepo2(wt)))
34266
34425
  return result;
@@ -34348,7 +34507,7 @@ var git = (cwd, args2, op) => exports_Effect.tryPromise({
34348
34507
  try: () => run4(cwd, "git", args2),
34349
34508
  catch: (cause3) => new GitError({ op, message: `git ${args2.join(" ")} threw`, cause: cause3 })
34350
34509
  }), isGitRepo2 = (dir) => exports_Effect.gen(function* () {
34351
- if (!existsSync9(dir))
34510
+ if (!existsSync10(dir))
34352
34511
  return false;
34353
34512
  const r = yield* git(dir, ["rev-parse", "--is-inside-work-tree"], "rev-parse");
34354
34513
  return r.code === 0 && r.stdout === "true";
@@ -34360,14 +34519,14 @@ var init_git_enforce = __esm(() => {
34360
34519
 
34361
34520
  // src/_impl/outbox.ts
34362
34521
  import { Database as Database2 } from "bun:sqlite";
34363
- import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
34364
- import { homedir } from "os";
34365
- import { join as join10 } from "path";
34522
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
34523
+ import { homedir as homedir2 } from "os";
34524
+ import { join as join11 } from "path";
34366
34525
  function ensureDb() {
34367
34526
  if (db)
34368
34527
  return db;
34369
- if (!existsSync10(MULTI_DIR3))
34370
- mkdirSync7(MULTI_DIR3, { recursive: true });
34528
+ if (!existsSync11(MULTI_DIR3))
34529
+ mkdirSync8(MULTI_DIR3, { recursive: true });
34371
34530
  db = new Database2(TASKS_DB_PATH2);
34372
34531
  db.exec(`
34373
34532
  CREATE TABLE IF NOT EXISTS stream_outbox (
@@ -34500,17 +34659,17 @@ function startOutboxFlusher(apiUrl, wsId) {
34500
34659
  var MULTI_DIR3, TASKS_DB_PATH2, BATCH_SIZE = 100, FLUSH_INTERVAL_MS = 2000, MAX_BACKOFF_MS = 30000, db = null;
34501
34660
  var init_outbox = __esm(() => {
34502
34661
  init_client();
34503
- MULTI_DIR3 = join10(homedir(), ".multi");
34504
- TASKS_DB_PATH2 = join10(MULTI_DIR3, "tasks.db");
34662
+ MULTI_DIR3 = join11(homedir2(), ".multi");
34663
+ TASKS_DB_PATH2 = join11(MULTI_DIR3, "tasks.db");
34505
34664
  });
34506
34665
 
34507
34666
  // src/_impl/run-task.ts
34508
- import { mkdirSync as mkdirSync8, existsSync as existsSync11, writeFileSync as writeFileSync6, readFileSync as readFileSync9, appendFileSync as appendFileSync4, unlinkSync as unlinkSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
34509
- import { join as join11, dirname as dirname9 } from "path";
34667
+ import { mkdirSync as mkdirSync9, existsSync as existsSync12, writeFileSync as writeFileSync7, readFileSync as readFileSync10, appendFileSync as appendFileSync4, unlinkSync as unlinkSync6, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
34668
+ import { join as join12, dirname as dirname9 } from "path";
34510
34669
  function ensureDirs() {
34511
- for (const d of [MULTI_DIR4, join11(MULTI_DIR4, "logs"), SKILLS_DIR2]) {
34512
- if (!existsSync11(d))
34513
- mkdirSync8(d, { recursive: true });
34670
+ for (const d of [MULTI_DIR4, join12(MULTI_DIR4, "logs"), SKILLS_DIR2]) {
34671
+ if (!existsSync12(d))
34672
+ mkdirSync9(d, { recursive: true });
34514
34673
  }
34515
34674
  }
34516
34675
  function log3(msg) {
@@ -34589,7 +34748,7 @@ async function writeTaskMemory(apiUrl, task, text) {
34589
34748
  async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34590
34749
  const issueId = task.issue_id;
34591
34750
  const isFollowup = !!task.followup;
34592
- const baseWorkingDir = task.working_dir && existsSync11(task.working_dir) ? task.working_dir : undefined;
34751
+ const baseWorkingDir = task.working_dir && existsSync12(task.working_dir) ? task.working_dir : undefined;
34593
34752
  const tenantWsId = task.tenant_workspace_id ?? null;
34594
34753
  const projectId = task.project_id ?? null;
34595
34754
  const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
@@ -34656,13 +34815,13 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34656
34815
  await postStream(apiUrl, issueId, "progress", { message: `Device ${deviceId} picked up ${isFollowup ? "follow-up" : "task"}` });
34657
34816
  let attachmentRefs = [];
34658
34817
  if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
34659
- const baseDir = workingDir || join11(MULTI_DIR4, "tmp", issueId);
34660
- const inDir = join11(baseDir, ".multi-in", task.from_comment_id);
34818
+ const baseDir = workingDir || join12(MULTI_DIR4, "tmp", issueId);
34819
+ const inDir = join12(baseDir, ".multi-in", task.from_comment_id);
34661
34820
  attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
34662
34821
  if (attachmentRefs.length)
34663
34822
  log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
34664
34823
  }
34665
- const outDir = join11(workingDir || join11(MULTI_DIR4, "tmp", issueId), ".multi-out");
34824
+ const outDir = join12(workingDir || join12(MULTI_DIR4, "tmp", issueId), ".multi-out");
34666
34825
  let liveCommentId;
34667
34826
  let liveBody = "";
34668
34827
  let hadError = false;
@@ -35172,7 +35331,12 @@ async function buildPlanningPreamble(apiUrl, task, _wsId) {
35172
35331
  if (depth >= PLANNING_DEPTH_LIMIT) {
35173
35332
  return `# Planning (sub-task context)
35174
35333
 
35175
- You are acting on a sub-issue spawned by another agent. You MAY emit a \`multi-plan\` block to update your own issue's status (e.g. mark done/failed) but CANNOT create child issues or delegate further.
35334
+ You are acting on a sub-issue spawned by another agent. You MAY emit a \`multi-plan\` block to update your own issue's status (e.g. mark done/blocked/failed) but CANNOT create child issues or delegate further.
35335
+
35336
+ Status guidance:
35337
+ - \`done\` — work is finished; no further human or agent action needed.
35338
+ - \`blocked\` — you need a human decision before continuing (confirmation, choice between approaches, missing info). Do NOT mark \`done\` when waiting on review.
35339
+ - \`failed\` — work could not be completed; explain in your reply.
35176
35340
 
35177
35341
  \`\`\`multi-plan
35178
35342
  {"actions":[{"type":"update","id":"<this issue id>","status":"done"}]}
@@ -35216,13 +35380,19 @@ Issue actions:
35216
35380
  {"type":"issue.delete","id":"<issue id or key>"},
35217
35381
  {"type":"issue.delete_where","status":"todo"},
35218
35382
  {"type":"issue.list","status":"todo","assignee_id":"<agent id>","limit":20},
35219
- {"type":"issue.search","query":"flaky tests","limit":10}
35383
+ {"type":"issue.search","query":"flaky tests","limit":10},
35384
+ {"type":"memory.search","query":"how does the deploy pipeline work","limit":10},
35385
+ {"type":"memory.write","text":"Long-form note worth remembering across sessions...","summary":"short index hint","kind":"agent_note"}
35220
35386
  ]}
35221
35387
  \`\`\`
35222
35388
 
35389
+ Status values: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when you are pausing to wait on a human decision** — confirmation, a choice between approaches, or missing context. Marking such an issue \`done\` is wrong: the work isn't finished, you're waiting on review. Use \`archived\` to hide a completed/abandoned issue from default views.
35390
+
35223
35391
  Prefer the bulk \`issue.delete_where\` over \`issue.list\` + per-issue \`issue.delete\` when the user's intent matches a filter ("delete all todo issues", "remove anything assigned to agent X"). It runs in one turn instead of two.
35224
35392
 
35225
- Read actions (\`issue.list\`, \`issue.search\`) return the matched rows in the action summary comment. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting).
35393
+ Read actions (\`issue.list\`, \`issue.search\`, \`memory.search\`) return the matched rows in the action summary comment. Use them to look up issue ids/keys before \`update\` / \`delegate\` / \`issue.delete\` ONLY when the per-row filter isn't enough (e.g. you need to inspect titles before acting).
35394
+
35395
+ Memory actions are project-scoped persistent storage (FTS5 + vector). Use \`memory.search\` to recall facts learned in past sessions; the hits land in the next-turn context like \`issue.search\`. Use \`memory.write\` sparingly to record durable notes (decisions, gotchas, references) that will be useful to future agents on this project — not transient task state. \`kind\` defaults to \`agent_note\`; use a stable kind string (e.g. \`decision\`, \`gotcha\`) if you want to filter later.
35226
35396
 
35227
35397
  Agent + skill self-service (use sparingly — only when you genuinely need a new capability that isn't covered by an existing agent or skill):
35228
35398
 
@@ -35272,7 +35442,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35272
35442
  results.push({ type: "note", status: "note", message: `${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})` });
35273
35443
  }
35274
35444
  }
35275
- const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3 };
35445
+ const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3, "memory.search": 5, "memory.write": 5 };
35276
35446
  const counts = {};
35277
35447
  actions = actions.filter((a) => {
35278
35448
  const cap = SUBCAPS[a.type];
@@ -35338,7 +35508,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35338
35508
  }
35339
35509
  lines.push(`- [ok] updated ${res.data.key}`);
35340
35510
  results.push({ type: "update", status: "ok", issue_id: res.data.id, key: res.data.key, status_to: a.status ?? null, title_to: a.title ?? null });
35341
- if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync11(parentTask.working_dir)) {
35511
+ if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync12(parentTask.working_dir)) {
35342
35512
  const targetKey = res.data?.key;
35343
35513
  if (targetKey) {
35344
35514
  const targetIssueId = res.data?.id || a.id;
@@ -35454,6 +35624,57 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35454
35624
  lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
35455
35625
  }
35456
35626
  results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
35627
+ } else if (a.type === "memory.search") {
35628
+ const projectId = a.project_id || parentProjectId;
35629
+ if (!projectId) {
35630
+ lines.push(`- [err] memory.search "${a.query}": no project_id`);
35631
+ results.push({ type: "memory.search", status: "error", error: "no project_id", label: a.query });
35632
+ continue;
35633
+ }
35634
+ const limit = a.limit ?? 10;
35635
+ const qs = new URLSearchParams({ project_id: projectId, q: a.query, limit: String(limit) });
35636
+ const res = await apiClient.get(`${apiUrl}/api/memory/search?${qs.toString()}`);
35637
+ if (!res.success) {
35638
+ lines.push(`- [err] memory.search "${a.query}": ${res.error || res.status}`);
35639
+ results.push({ type: "memory.search", status: "error", error: String(res.error || res.status), label: a.query });
35640
+ continue;
35641
+ }
35642
+ const rows = Array.isArray(res.data?.rows) ? res.data.rows : [];
35643
+ if (!rows.length) {
35644
+ lines.push(`- [ok] memory.search "${a.query}": 0 hits`);
35645
+ results.push({ type: "memory.search", status: "ok", count: 0, query: a.query });
35646
+ continue;
35647
+ }
35648
+ lines.push(`- [ok] memory.search "${a.query}": ${rows.length} hit(s)`);
35649
+ for (const r of rows) {
35650
+ const summary5 = (r.summary || r.text || "").replace(/\s+/g, " ").slice(0, 200);
35651
+ lines.push(` - [${r.source_kind || "memory"}] ${summary5}`);
35652
+ }
35653
+ results.push({ type: "memory.search", status: "ok", count: rows.length, query: a.query });
35654
+ } else if (a.type === "memory.write") {
35655
+ const projectId = a.project_id || parentProjectId;
35656
+ if (!projectId) {
35657
+ lines.push(`- [err] memory.write: no project_id`);
35658
+ results.push({ type: "memory.write", status: "error", error: "no project_id" });
35659
+ continue;
35660
+ }
35661
+ const body = {
35662
+ project_id: projectId,
35663
+ source_kind: a.kind || "agent_note",
35664
+ source_id: a.source_id || `${parentTask.agent_id || "agent"}:${parentTask.issue_id || ""}:${Date.now()}`,
35665
+ agent_id: parentTask.agent_id || null,
35666
+ text: a.text,
35667
+ summary: a.summary || null
35668
+ };
35669
+ const res = await apiClient.post(`${apiUrl}/api/memory/write`, body);
35670
+ if (!res.success) {
35671
+ lines.push(`- [err] memory.write: ${res.error || res.status}`);
35672
+ results.push({ type: "memory.write", status: "error", error: String(res.error || res.status) });
35673
+ continue;
35674
+ }
35675
+ const row = res.data?.row || {};
35676
+ lines.push(`- [ok] memory.write -> ${row.id || "(no id)"}${row.embedding_id ? " (embedded)" : ""}`);
35677
+ results.push({ type: "memory.write", status: "ok", memory_id: row.id, embedded: !!row.embedding_id });
35457
35678
  } else if (a.type === "agent.create") {
35458
35679
  if (!parentWsId) {
35459
35680
  lines.push(`- [err] agent.create "${a.name}": no tenant workspace id`);
@@ -35619,26 +35840,26 @@ function statusIcon(status3) {
35619
35840
  }
35620
35841
  }
35621
35842
  async function resolveAcpAdapter(agentType, detectedPath) {
35622
- if (agentType === "pi" && detectedPath && existsSync11(detectedPath)) {
35843
+ if (agentType === "pi" && detectedPath && existsSync12(detectedPath)) {
35623
35844
  return [detectedPath, "--mode", "rpc"];
35624
35845
  }
35625
35846
  const override = process.env.MULTI_ACP_ADAPTER?.trim();
35626
35847
  const adapterNames = override ? [override] : ["claude-code-acp", "claude-agent-acp"];
35627
35848
  for (const name of adapterNames) {
35628
- if (name.startsWith("/") && existsSync11(name))
35849
+ if (name.startsWith("/") && existsSync12(name))
35629
35850
  return [name];
35630
35851
  try {
35631
35852
  const here = new URL(import.meta.url).pathname;
35632
35853
  let dir = here;
35633
35854
  for (let i = 0;i < 8; i++) {
35634
35855
  dir = dirname9(dir);
35635
- const bin = join11(dir, "node_modules", ".bin", name);
35636
- if (existsSync11(bin))
35856
+ const bin = join12(dir, "node_modules", ".bin", name);
35857
+ if (existsSync12(bin))
35637
35858
  return [bin];
35638
35859
  }
35639
35860
  } catch {}
35640
- const global = join11(HOME4, ".bun", "install", "global", "node_modules", ".bin", name);
35641
- if (existsSync11(global))
35861
+ const global = join12(HOME4, ".bun", "install", "global", "node_modules", ".bin", name);
35862
+ if (existsSync12(global))
35642
35863
  return [global];
35643
35864
  }
35644
35865
  return null;
@@ -35647,7 +35868,7 @@ async function postStream(_apiUrl, issueId, event_type, payload) {
35647
35868
  try {
35648
35869
  ensureDirs();
35649
35870
  const date6 = new Date().toISOString().slice(0, 10);
35650
- const path = join11(MULTI_DIR4, "logs", `events-${date6}.ndjson`);
35871
+ const path = join12(MULTI_DIR4, "logs", `events-${date6}.ndjson`);
35651
35872
  appendFileSync4(path, JSON.stringify({ ts: Date.now(), issue_id: issueId, event_type, payload }) + `
35652
35873
  `);
35653
35874
  } catch {}
@@ -35666,7 +35887,7 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
35666
35887
  const items = list.data?.results || list.data || [];
35667
35888
  if (!Array.isArray(items) || items.length === 0)
35668
35889
  return [];
35669
- mkdirSync8(destDir, { recursive: true });
35890
+ mkdirSync9(destDir, { recursive: true });
35670
35891
  const token = authTokenHeader();
35671
35892
  const out = [];
35672
35893
  for (const it of items) {
@@ -35675,8 +35896,8 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
35675
35896
  continue;
35676
35897
  const buf = new Uint8Array(await res.arrayBuffer());
35677
35898
  const safe = it.filename.replace(/[^A-Za-z0-9._-]/g, "_");
35678
- const p = join11(destDir, safe);
35679
- writeFileSync6(p, buf);
35899
+ const p = join12(destDir, safe);
35900
+ writeFileSync7(p, buf);
35680
35901
  out.push({ filename: it.filename, path: p });
35681
35902
  }
35682
35903
  return out;
@@ -35686,9 +35907,9 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
35686
35907
  }
35687
35908
  function authTokenHeader() {
35688
35909
  try {
35689
- if (!existsSync11(CONFIG_PATH2))
35910
+ if (!existsSync12(CONFIG_PATH2))
35690
35911
  return null;
35691
- const raw = JSON.parse(readFileSync9(CONFIG_PATH2, "utf8"));
35912
+ const raw = JSON.parse(readFileSync10(CONFIG_PATH2, "utf8"));
35692
35913
  const token = raw.token ?? raw.authToken;
35693
35914
  return token ? `Bearer ${token}` : null;
35694
35915
  } catch {
@@ -35696,14 +35917,14 @@ function authTokenHeader() {
35696
35917
  }
35697
35918
  }
35698
35919
  async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
35699
- if (!existsSync11(dir))
35920
+ if (!existsSync12(dir))
35700
35921
  return 0;
35701
35922
  const files = [];
35702
35923
  const walk = (d, depth = 0) => {
35703
35924
  if (depth > 3)
35704
35925
  return;
35705
- for (const name of readdirSync2(d)) {
35706
- const p = join11(d, name);
35926
+ for (const name of readdirSync3(d)) {
35927
+ const p = join12(d, name);
35707
35928
  try {
35708
35929
  const st = statSync2(p);
35709
35930
  if (st.isDirectory())
@@ -35721,7 +35942,7 @@ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir)
35721
35942
  let uploaded = 0;
35722
35943
  for (const f of files) {
35723
35944
  try {
35724
- const data = readFileSync9(f);
35945
+ const data = readFileSync10(f);
35725
35946
  const form = new FormData;
35726
35947
  const blob = new Blob([data]);
35727
35948
  form.append("file", blob, f.split("/").pop() || "file");
@@ -35736,7 +35957,7 @@ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir)
35736
35957
  if (res.ok) {
35737
35958
  uploaded++;
35738
35959
  try {
35739
- unlinkSync5(f);
35960
+ unlinkSync6(f);
35740
35961
  } catch {}
35741
35962
  }
35742
35963
  } catch (e) {
@@ -36012,10 +36233,10 @@ var init_run_task = __esm(() => {
36012
36233
  init_git_enforce();
36013
36234
  init_outbox();
36014
36235
  HOME4 = process.env.HOME || process.env.USERPROFILE || ".";
36015
- MULTI_DIR4 = join11(HOME4, ".multi");
36016
- CONFIG_PATH2 = join11(MULTI_DIR4, "config.json");
36017
- LOG_PATH3 = join11(MULTI_DIR4, "logs", "agent.log");
36018
- SKILLS_DIR2 = join11(MULTI_DIR4, "skills");
36236
+ MULTI_DIR4 = join12(HOME4, ".multi");
36237
+ CONFIG_PATH2 = join12(MULTI_DIR4, "config.json");
36238
+ LOG_PATH3 = join12(MULTI_DIR4, "logs", "agent.log");
36239
+ SKILLS_DIR2 = join12(MULTI_DIR4, "skills");
36019
36240
  });
36020
36241
 
36021
36242
  // ../lib/chat-doc.ts
@@ -36061,12 +36282,12 @@ function patchMessage(doc2, mapId, patch9) {
36061
36282
  map20.set(k, v);
36062
36283
  }
36063
36284
  }
36064
- function appendText(doc2, mapId, chunk2) {
36285
+ function appendText(doc2, mapId, chunk3) {
36065
36286
  const map20 = doc2.getContainerById(mapId);
36066
36287
  if (!map20)
36067
36288
  return;
36068
36289
  const cur = map20.get("text") ?? "";
36069
- map20.set("text", cur + chunk2);
36290
+ map20.set("text", cur + chunk3);
36070
36291
  }
36071
36292
  function finalizeMessage(doc2, mapId) {
36072
36293
  const map20 = doc2.getContainerById(mapId);
@@ -36107,8 +36328,8 @@ var init_chat_doc = __esm(() => {
36107
36328
  });
36108
36329
 
36109
36330
  // src/_impl/chat-peer.ts
36110
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
36111
- import { dirname as dirname10, join as join12 } from "path";
36331
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
36332
+ import { dirname as dirname10, join as join13 } from "path";
36112
36333
  import { LoroDoc as LoroDoc2 } from "loro-crdt";
36113
36334
 
36114
36335
  class ChatPeer {
@@ -36125,24 +36346,24 @@ class ChatPeer {
36125
36346
  constructor(opts) {
36126
36347
  this.opts = opts;
36127
36348
  this.chatId = opts.chatId;
36128
- this.snapshotPath = join12(MULTI_DIR, "chats", `${opts.chatId}.loro`);
36349
+ this.snapshotPath = join13(MULTI_DIR, "chats", `${opts.chatId}.loro`);
36129
36350
  this.doc = this.loadFromDisk();
36130
36351
  for (const m of listMessages(this.doc))
36131
36352
  this.seenIds.add(m.id);
36132
36353
  }
36133
36354
  loadFromDisk() {
36134
36355
  try {
36135
- if (existsSync12(this.snapshotPath)) {
36136
- const bytes = readFileSync10(this.snapshotPath);
36356
+ if (existsSync13(this.snapshotPath)) {
36357
+ const bytes = readFileSync11(this.snapshotPath);
36137
36358
  return importSnapshot(new Uint8Array(bytes));
36138
36359
  }
36139
36360
  } catch {}
36140
36361
  return new LoroDoc2;
36141
36362
  }
36142
36363
  persist() {
36143
- if (!existsSync12(dirname10(this.snapshotPath)))
36144
- mkdirSync9(dirname10(this.snapshotPath), { recursive: true });
36145
- writeFileSync7(this.snapshotPath, exportSnapshot(this.doc));
36364
+ if (!existsSync13(dirname10(this.snapshotPath)))
36365
+ mkdirSync10(dirname10(this.snapshotPath), { recursive: true });
36366
+ writeFileSync8(this.snapshotPath, exportSnapshot(this.doc));
36146
36367
  this.dirtySinceWrite = 0;
36147
36368
  }
36148
36369
  appendAndPush(msg) {
@@ -36177,8 +36398,8 @@ class ChatPeer {
36177
36398
  this.flush();
36178
36399
  return { msgId: msg.id, containerId };
36179
36400
  }
36180
- appendPartialText(containerId, chunk2) {
36181
- appendText(this.doc, containerId, chunk2);
36401
+ appendPartialText(containerId, chunk3) {
36402
+ appendText(this.doc, containerId, chunk3);
36182
36403
  this.flush();
36183
36404
  }
36184
36405
  finalizePartialMessage(containerId) {
@@ -36685,6 +36906,58 @@ async function executeChatPlanActions(actionsIn, parseErrors, ctx) {
36685
36906
  lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
36686
36907
  results.push({ type: "issue.search", status: "ok", count: rows.length, query: a.query });
36687
36908
  tally(true);
36909
+ } else if (a.type === "memory.search") {
36910
+ const project_id = a.project_id || ctx.projectId;
36911
+ if (!project_id) {
36912
+ lines.push(`- [err] memory.search "${a.query}": project_id required (chat has no pinned project)`);
36913
+ results.push({ type: "memory.search", status: "error", error: "project_id required (chat has no pinned project)", label: a.query });
36914
+ tally(false);
36915
+ continue;
36916
+ }
36917
+ const limit = a.limit ?? 10;
36918
+ const qs = new URLSearchParams({ project_id, q: a.query, limit: String(limit) });
36919
+ const res = await apiClient.get(`${ctx.apiUrl}/api/memory/search?${qs.toString()}`);
36920
+ if (!res.success) {
36921
+ lines.push(`- [err] memory.search "${a.query}": ${res.error || res.status}`);
36922
+ results.push({ type: "memory.search", status: "error", error: String(res.error || res.status), label: a.query });
36923
+ tally(false);
36924
+ continue;
36925
+ }
36926
+ const rows = Array.isArray(res.data?.rows) ? res.data.rows : [];
36927
+ lines.push(`- [ok] memory.search "${a.query}": ${rows.length} hit(s)`);
36928
+ for (const r of rows) {
36929
+ const summary5 = (r.summary || r.text || "").replace(/\s+/g, " ").slice(0, 200);
36930
+ lines.push(` - [${r.source_kind || "memory"}] ${summary5}`);
36931
+ }
36932
+ results.push({ type: "memory.search", status: "ok", count: rows.length, query: a.query });
36933
+ tally(true);
36934
+ } else if (a.type === "memory.write") {
36935
+ const project_id = a.project_id || ctx.projectId;
36936
+ if (!project_id) {
36937
+ lines.push(`- [err] memory.write: project_id required (chat has no pinned project)`);
36938
+ results.push({ type: "memory.write", status: "error", error: "project_id required (chat has no pinned project)" });
36939
+ tally(false);
36940
+ continue;
36941
+ }
36942
+ const body = {
36943
+ project_id,
36944
+ source_kind: a.kind || "agent_note",
36945
+ source_id: a.source_id || `chat:${ctx.chatId}:${Date.now()}`,
36946
+ agent_id: ctx.agentId || null,
36947
+ text: a.text,
36948
+ summary: a.summary || null
36949
+ };
36950
+ const res = await apiClient.post(`${ctx.apiUrl}/api/memory/write`, body);
36951
+ if (!res.success) {
36952
+ lines.push(`- [err] memory.write: ${res.error || res.status}`);
36953
+ results.push({ type: "memory.write", status: "error", error: String(res.error || res.status) });
36954
+ tally(false);
36955
+ continue;
36956
+ }
36957
+ const row = res.data?.row || {};
36958
+ lines.push(`- [ok] memory.write -> ${row.id || "(no id)"}${row.embedding_id ? " (embedded)" : ""}`);
36959
+ results.push({ type: "memory.write", status: "ok", memory_id: row.id, embedded: !!row.embedding_id });
36960
+ tally(true);
36688
36961
  } else if (a.type === "agent.create") {
36689
36962
  const res = await apiClient.post(agentsMutateUrl, {
36690
36963
  action: "create",
@@ -36800,7 +37073,9 @@ var init_chat_plan_actions = __esm(() => {
36800
37073
  "skill.detach": 5,
36801
37074
  "agent.update": 5,
36802
37075
  "session.create": 3,
36803
- "issue.comment": 5
37076
+ "issue.comment": 5,
37077
+ "memory.search": 5,
37078
+ "memory.write": 5
36804
37079
  };
36805
37080
  });
36806
37081
 
@@ -37178,6 +37453,8 @@ Action vocabulary:
37178
37453
  {"type":"issue.delete_where","status":"todo","limit":50},
37179
37454
  {"type":"issue.list","status":"todo","limit":20},
37180
37455
  {"type":"issue.search","query":"flaky tests","limit":10},
37456
+ {"type":"memory.search","query":"deploy pipeline","limit":10},
37457
+ {"type":"memory.write","text":"durable note worth recalling next session","summary":"short hint","kind":"agent_note"},
37181
37458
  {"type":"agent.create","name":"refactor-bot","prompt":"...","allowed_tools":["Read","Edit","Bash"]},
37182
37459
  {"type":"agent.update","id":"ag_xxx","prompt":"..."},
37183
37460
  {"type":"skill.create","name":"run-tests","description":"...","body":"---\\nname: run-tests\\n---\\n..."},
@@ -37188,7 +37465,9 @@ Action vocabulary:
37188
37465
 
37189
37466
  Rules:
37190
37467
  - Omit the block entirely if no actions are needed. Don't emit empty arrays.
37191
- - Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5.
37468
+ - Status values for \`update\`: \`todo\` | \`in_progress\` | \`blocked\` | \`done\` | \`archived\` | \`failed\`. **Use \`blocked\` (NOT \`done\`) when the issue is paused waiting on a human decision** — confirmation, choice, or missing context. Marking such an issue \`done\` misrepresents finished work. Use \`archived\` to hide a completed/abandoned issue from default views.
37469
+ - Max 10 actions per turn. Sub-caps: agent.create=2, skill.create=3, agent.update=5, skill.attach/detach=5, session.create=3, issue.comment=5, memory.search=5, memory.write=5.
37470
+ - Memory actions are project-scoped persistent notes (FTS5 + vector). \`memory.search\` returns hits in the action summary; the agent reads them next turn. \`memory.write\` records durable facts for future sessions — use sparingly, not for transient task state. Both require a pinned project.
37192
37471
  - Chat-initiated agent.create / agent.update / skill.attach are auto-approved (the user is reading this reply right now). skill.create still queues for human review.
37193
37472
  - Use \`issue.comment\` with \`@<agent name>\` mention to dispatch an agent on an existing issue. Plain comments without an @mention are recorded but do not trigger a run. Issues whose autonomy is \`manual\` will not dispatch.
37194
37473
  - To create an issue AND assign/dispatch it to an agent in the same turn, set \`assignee_type\` + \`assignee_id\` directly on the \`create\` action. Do NOT emit a separate \`delegate\` or \`issue.comment\` action referring to a brand-new issue in the same plan block — actions execute as a flat list and the new issue's id/key is not available to later actions in the same block. \`delegate\` and \`issue.comment\` are for issues that already exist before this turn.
@@ -37962,7 +38241,7 @@ import { parseArgs } from "util";
37962
38241
  // package.json
37963
38242
  var package_default = {
37964
38243
  name: "@shipers-dev/multi",
37965
- version: "0.49.0",
38244
+ version: "0.52.0",
37966
38245
  type: "module",
37967
38246
  bin: {
37968
38247
  "multi-agent": "./dist/index.js"
@@ -38476,7 +38755,7 @@ class Runners extends exports_Effect.Service()("cli/Runners", {
38476
38755
  // src/commands/simple.ts
38477
38756
  init_esm();
38478
38757
  init_paths();
38479
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
38758
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
38480
38759
 
38481
38760
  // src/services/Config.ts
38482
38761
  init_esm();
@@ -38571,7 +38850,7 @@ var statusCmd = exports_Effect.fn("statusCmd")(function* () {
38571
38850
  process.exit(1);
38572
38851
  }
38573
38852
  const d = res.data;
38574
- const pid = existsSync6(PID_PATH) ? readFileSync6(PID_PATH, "utf8").trim() : null;
38853
+ const pid = existsSync7(PID_PATH) ? readFileSync7(PID_PATH, "utf8").trim() : null;
38575
38854
  const daemon = pid && isRunning3(Number(pid)) ? `running (pid ${pid})` : "stopped";
38576
38855
  console.log(`
38577
38856
  Device Status
@@ -38586,11 +38865,11 @@ Daemon: ${daemon}
38586
38865
  });
38587
38866
  var stopCmd = exports_Effect.fn("stopCmd")(function* () {
38588
38867
  const fs3 = yield* FileSystem;
38589
- if (!existsSync6(PID_PATH)) {
38868
+ if (!existsSync7(PID_PATH)) {
38590
38869
  console.log("No daemon running.");
38591
38870
  return;
38592
38871
  }
38593
- const pid = Number(readFileSync6(PID_PATH, "utf8").trim());
38872
+ const pid = Number(readFileSync7(PID_PATH, "utf8").trim());
38594
38873
  if (!pid || !isRunning3(pid)) {
38595
38874
  yield* fs3.remove(PID_PATH);
38596
38875
  console.log("Cleaned stale pidfile.");
@@ -38603,11 +38882,11 @@ var stopCmd = exports_Effect.fn("stopCmd")(function* () {
38603
38882
  } catch {}
38604
38883
  });
38605
38884
  var logsCmd = exports_Effect.fn("logsCmd")(function* () {
38606
- if (!existsSync6(LOG_PATH)) {
38885
+ if (!existsSync7(LOG_PATH)) {
38607
38886
  console.log("No logs yet.");
38608
38887
  return;
38609
38888
  }
38610
- const content = readFileSync6(LOG_PATH, "utf8");
38889
+ const content = readFileSync7(LOG_PATH, "utf8");
38611
38890
  console.log(content.split(`
38612
38891
  `).slice(-100).join(`
38613
38892
  `));
@@ -38616,10 +38895,10 @@ var resetCmd = exports_Effect.fn("resetCmd")(function* (issueId) {
38616
38895
  if (!issueId) {
38617
38896
  return yield* exports_Effect.fail(new UsageError({ message: "reset requires --issue <id>" }));
38618
38897
  }
38619
- if (!existsSync6(PORT_PATH)) {
38898
+ if (!existsSync7(PORT_PATH)) {
38620
38899
  return yield* exports_Effect.fail(new DaemonError({ message: "Daemon not running (no port file)." }));
38621
38900
  }
38622
- const port = Number(readFileSync6(PORT_PATH, "utf8").trim());
38901
+ const port = Number(readFileSync7(PORT_PATH, "utf8").trim());
38623
38902
  const config2 = yield* Config4;
38624
38903
  const cfg = yield* config2.load;
38625
38904
  if (!cfg.dispatchSecret) {
@@ -38780,10 +39059,10 @@ var linkCmd = exports_Effect.fn("linkCmd")(function* (apiUrl, agentId) {
38780
39059
  // src/commands/restart.ts
38781
39060
  init_esm();
38782
39061
  init_paths();
38783
- import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync4 } from "fs";
39062
+ import { existsSync as existsSync9, readFileSync as readFileSync9, unlinkSync as unlinkSync5 } from "fs";
38784
39063
 
38785
39064
  // src/_impl/pid-lock.ts
38786
- import { closeSync, existsSync as existsSync7, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeSync } from "fs";
39065
+ import { closeSync, existsSync as existsSync8, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync8, unlinkSync as unlinkSync4, writeSync } from "fs";
38787
39066
  import { dirname as dirname8 } from "path";
38788
39067
  var isAlive = (pid) => {
38789
39068
  if (!Number.isFinite(pid) || pid <= 0)
@@ -38796,7 +39075,7 @@ var isAlive = (pid) => {
38796
39075
  }
38797
39076
  };
38798
39077
  var writeExclusive = (path, pid) => {
38799
- mkdirSync6(dirname8(path), { recursive: true });
39078
+ mkdirSync7(dirname8(path), { recursive: true });
38800
39079
  const fd = openSync(path, "wx");
38801
39080
  try {
38802
39081
  writeSync(fd, String(pid));
@@ -38815,7 +39094,7 @@ var acquirePidLock = (path) => {
38815
39094
  }
38816
39095
  const raw = (() => {
38817
39096
  try {
38818
- return readFileSync7(path, "utf8").trim();
39097
+ return readFileSync8(path, "utf8").trim();
38819
39098
  } catch {
38820
39099
  return "";
38821
39100
  }
@@ -38827,7 +39106,7 @@ var acquirePidLock = (path) => {
38827
39106
  return { ok: false, existingPid };
38828
39107
  }
38829
39108
  try {
38830
- unlinkSync3(path);
39109
+ unlinkSync4(path);
38831
39110
  } catch {}
38832
39111
  try {
38833
39112
  writeExclusive(path, ourPid);
@@ -38837,7 +39116,7 @@ var acquirePidLock = (path) => {
38837
39116
  throw e;
38838
39117
  const racingRaw = (() => {
38839
39118
  try {
38840
- return readFileSync7(path, "utf8").trim();
39119
+ return readFileSync8(path, "utf8").trim();
38841
39120
  } catch {
38842
39121
  return "";
38843
39122
  }
@@ -38848,12 +39127,12 @@ var acquirePidLock = (path) => {
38848
39127
  };
38849
39128
  var releasePidLock = (path) => {
38850
39129
  try {
38851
- if (!existsSync7(path))
39130
+ if (!existsSync8(path))
38852
39131
  return;
38853
- const raw = readFileSync7(path, "utf8").trim();
39132
+ const raw = readFileSync8(path, "utf8").trim();
38854
39133
  if (Number(raw) !== process.pid)
38855
39134
  return;
38856
- unlinkSync3(path);
39135
+ unlinkSync4(path);
38857
39136
  } catch {}
38858
39137
  };
38859
39138
  var killStaleConnects = () => {
@@ -38918,8 +39197,8 @@ var isRunning4 = (pid) => {
38918
39197
  var restartCmd = exports_Effect.fn("restartCmd")(function* (apiUrl) {
38919
39198
  const fs3 = yield* FileSystem;
38920
39199
  yield* (yield* Config4).ensureDirs;
38921
- if (existsSync8(PID_PATH)) {
38922
- const pid = Number(readFileSync8(PID_PATH, "utf8").trim());
39200
+ if (existsSync9(PID_PATH)) {
39201
+ const pid = Number(readFileSync9(PID_PATH, "utf8").trim());
38923
39202
  if (pid && isRunning4(pid)) {
38924
39203
  yield* fs3.writeText(STOP_PATH, "1");
38925
39204
  try {
@@ -38938,13 +39217,13 @@ var restartCmd = exports_Effect.fn("restartCmd")(function* (apiUrl) {
38938
39217
  }
38939
39218
  }
38940
39219
  try {
38941
- if (existsSync8(PID_PATH))
38942
- unlinkSync4(PID_PATH);
39220
+ if (existsSync9(PID_PATH))
39221
+ unlinkSync5(PID_PATH);
38943
39222
  } catch {}
38944
39223
  }
38945
39224
  try {
38946
- if (existsSync8(STOP_PATH))
38947
- unlinkSync4(STOP_PATH);
39225
+ if (existsSync9(STOP_PATH))
39226
+ unlinkSync5(STOP_PATH);
38948
39227
  } catch {}
38949
39228
  const killed = killStaleConnects();
38950
39229
  if (killed.length)
@@ -39040,21 +39319,22 @@ init_client();
39040
39319
  init_detect();
39041
39320
  init_run_task();
39042
39321
  import { Database as Database3 } from "bun:sqlite";
39043
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6 } from "fs";
39044
- import { homedir as homedir2 } from "os";
39045
- import { join as join13 } from "path";
39322
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync7 } from "fs";
39323
+ import { homedir as homedir3 } from "os";
39324
+ import { join as join14 } from "path";
39325
+ init_adapter_pidfile();
39046
39326
  init_errors();
39047
- var MULTI_DIR5 = join13(homedir2(), ".multi");
39048
- var PID_PATH2 = join13(MULTI_DIR5, "agent.pid");
39049
- var PORT_PATH2 = join13(MULTI_DIR5, "agent.port");
39050
- var STOP_PATH2 = join13(MULTI_DIR5, "stop.flag");
39051
- var TASKS_DB_PATH3 = join13(MULTI_DIR5, "tasks.db");
39327
+ var MULTI_DIR5 = join14(homedir3(), ".multi");
39328
+ var PID_PATH2 = join14(MULTI_DIR5, "agent.pid");
39329
+ var PORT_PATH2 = join14(MULTI_DIR5, "agent.port");
39330
+ var STOP_PATH2 = join14(MULTI_DIR5, "stop.flag");
39331
+ var TASKS_DB_PATH3 = join14(MULTI_DIR5, "tasks.db");
39052
39332
  function ensureDirs2() {
39053
- if (!existsSync13(MULTI_DIR5))
39054
- mkdirSync10(MULTI_DIR5, { recursive: true });
39055
- const logs = join13(MULTI_DIR5, "logs");
39056
- if (!existsSync13(logs))
39057
- mkdirSync10(logs, { recursive: true });
39333
+ if (!existsSync14(MULTI_DIR5))
39334
+ mkdirSync11(MULTI_DIR5, { recursive: true });
39335
+ const logs = join14(MULTI_DIR5, "logs");
39336
+ if (!existsSync14(logs))
39337
+ mkdirSync11(logs, { recursive: true });
39058
39338
  }
39059
39339
  function isLocalApi(url2) {
39060
39340
  try {
@@ -39346,8 +39626,11 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39346
39626
  if (cfg.authToken)
39347
39627
  setAuthToken(cfg.authToken);
39348
39628
  ensureDirs2();
39349
- if (existsSync13(STOP_PATH2))
39350
- unlinkSync6(STOP_PATH2);
39629
+ if (existsSync14(STOP_PATH2))
39630
+ unlinkSync7(STOP_PATH2);
39631
+ const reaped = reapStaleAdapters((m) => log3(m));
39632
+ if (reaped > 0)
39633
+ log3(`reaped ${reaped} stale adapter${reaped === 1 ? "" : "s"}`);
39351
39634
  const detected = yield* exports_Effect.promise(() => detectAgents());
39352
39635
  const localMode = isLocalApi(apiUrl);
39353
39636
  log3(`daemon device=${cfg.deviceId} pid=${process.pid}`);
@@ -39481,10 +39764,10 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39481
39764
  });
39482
39765
  log3(`Local server: http://127.0.0.1:${port}`);
39483
39766
  try {
39484
- writeFileSync8(PORT_PATH2, String(port));
39767
+ writeFileSync9(PORT_PATH2, String(port));
39485
39768
  } catch {}
39486
39769
  try {
39487
- writeFileSync8(PID_PATH2, String(process.pid));
39770
+ writeFileSync9(PID_PATH2, String(process.pid));
39488
39771
  } catch {}
39489
39772
  let tunnel;
39490
39773
  if (localMode) {
@@ -39548,7 +39831,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39548
39831
  let probeFailures = 0;
39549
39832
  while (true) {
39550
39833
  yield* exports_Effect.sleep(exports_Duration.seconds(120));
39551
- if (existsSync13(STOP_PATH2)) {
39834
+ if (existsSync14(STOP_PATH2)) {
39552
39835
  yield* exports_Deferred.succeed(stopDeferred, "stop flag");
39553
39836
  return;
39554
39837
  }
@@ -39573,7 +39856,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39573
39856
  yield* exports_Effect.forkIn(daemonScope)(exports_Effect.gen(function* () {
39574
39857
  while (true) {
39575
39858
  yield* exports_Effect.sleep(exports_Duration.seconds(5));
39576
- if (existsSync13(STOP_PATH2)) {
39859
+ if (existsSync14(STOP_PATH2)) {
39577
39860
  yield* exports_Deferred.succeed(stopDeferred, "stop flag");
39578
39861
  return;
39579
39862
  }
@@ -39613,12 +39896,12 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39613
39896
  yield* exports_Effect.race(exports_Fiber.interruptAll(inFlight), exports_Effect.sleep(exports_Duration.seconds(10)));
39614
39897
  }
39615
39898
  yield* exports_Scope.close(daemonScope, exports_Exit.void).pipe(exports_Effect.catchAll(() => exports_Effect.void));
39616
- if (existsSync13(PID_PATH2))
39617
- unlinkSync6(PID_PATH2);
39618
- if (existsSync13(STOP_PATH2))
39619
- unlinkSync6(STOP_PATH2);
39620
- if (existsSync13(PORT_PATH2))
39621
- unlinkSync6(PORT_PATH2);
39899
+ if (existsSync14(PID_PATH2))
39900
+ unlinkSync7(PID_PATH2);
39901
+ if (existsSync14(STOP_PATH2))
39902
+ unlinkSync7(STOP_PATH2);
39903
+ if (existsSync14(PORT_PATH2))
39904
+ unlinkSync7(PORT_PATH2);
39622
39905
  db2.close();
39623
39906
  log3("disconnected");
39624
39907
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.49.0",
3
+ "version": "0.52.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"