@shipers-dev/multi 0.48.0 → 0.51.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 +448 -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);
@@ -33210,6 +33341,8 @@ async function runAcp(opts) {
33210
33341
  let activeSessionId = opts.sessionId || null;
33211
33342
  let recording = false;
33212
33343
  let chunkCount = 0;
33344
+ let capturingSummary = false;
33345
+ let summaryBuf = "";
33213
33346
  const alwaysAllow = new Set;
33214
33347
  const client = {
33215
33348
  async sessionUpdate(params) {
@@ -33222,7 +33355,7 @@ async function runAcp(opts) {
33222
33355
  },
33223
33356
  async readTextFile(params) {
33224
33357
  try {
33225
- const content = readFileSync5(params.path, "utf8");
33358
+ const content = readFileSync6(params.path, "utf8");
33226
33359
  const sliced = typeof params.line === "number" && typeof params.limit === "number" ? content.split(`
33227
33360
  `).slice(params.line, params.line + params.limit).join(`
33228
33361
  `) : content;
@@ -33234,9 +33367,9 @@ async function runAcp(opts) {
33234
33367
  async writeTextFile(params) {
33235
33368
  try {
33236
33369
  const dir = dirname6(params.path);
33237
- if (!existsSync5(dir))
33238
- mkdirSync5(dir, { recursive: true });
33239
- writeFileSync5(params.path, params.content, "utf8");
33370
+ if (!existsSync6(dir))
33371
+ mkdirSync6(dir, { recursive: true });
33372
+ writeFileSync6(params.path, params.content, "utf8");
33240
33373
  return {};
33241
33374
  } catch (e) {
33242
33375
  throw new Error(`writeTextFile failed: ${fmtErr(e)}`);
@@ -33304,15 +33437,41 @@ async function runAcp(opts) {
33304
33437
  await opts.onEvent({ event_type: "error", payload: { message: `agent produced no output (stopReason=${stopReason})` } });
33305
33438
  }
33306
33439
  await opts.onEvent({ event_type: "result", payload: { stopReason } });
33307
- return { stopReason, sessionId: activeSessionId };
33440
+ let summaryText;
33441
+ if (opts.summaryPrompt && chunkCount > 0) {
33442
+ try {
33443
+ capturingSummary = true;
33444
+ summaryBuf = "";
33445
+ await conn.prompt({
33446
+ sessionId: activeSessionId,
33447
+ prompt: [{ type: "text", text: opts.summaryPrompt }]
33448
+ });
33449
+ summaryText = summaryBuf.trim() || undefined;
33450
+ } catch (e) {
33451
+ await opts.onEvent({ event_type: "progress", payload: { message: `memory summary failed: ${fmtErr(e)}` } });
33452
+ } finally {
33453
+ capturingSummary = false;
33454
+ }
33455
+ }
33456
+ return { stopReason, sessionId: activeSessionId, summaryText };
33308
33457
  } finally {
33309
33458
  try {
33310
33459
  child.kill();
33311
33460
  } catch {}
33461
+ if (typeof child.pid === "number")
33462
+ removeAdapterPidfile(child.pid);
33312
33463
  }
33313
33464
  async function handleSessionUpdate(params, o) {
33314
33465
  const u = params.update;
33315
33466
  const kind = u.sessionUpdate;
33467
+ if (capturingSummary) {
33468
+ if (kind === "agent_message_chunk") {
33469
+ const text = extractText(u.content);
33470
+ if (text)
33471
+ summaryBuf += text;
33472
+ }
33473
+ return;
33474
+ }
33316
33475
  switch (kind) {
33317
33476
  case "agent_message_chunk": {
33318
33477
  const text = extractText(u.content);
@@ -33453,17 +33612,111 @@ var init_acp_runner = __esm(() => {
33453
33612
  init_acp();
33454
33613
  init_client();
33455
33614
  init_workspace_mutex();
33615
+ init_adapter_pidfile();
33456
33616
  });
33457
33617
 
33458
33618
  // src/_impl/acpx-runner.ts
33459
33619
  import { appendFileSync as appendFileSync3 } from "fs";
33460
- import { join as join8 } from "path";
33620
+ import { join as join9 } from "path";
33461
33621
  function dlog(msg) {
33462
33622
  try {
33463
33623
  appendFileSync3(LOG_PATH2, `[${new Date().toISOString()}] ${msg}
33464
33624
  `);
33465
33625
  } catch {}
33466
33626
  }
33627
+ async function spawnAcpxPrompt(agentType, cwd, sessionName, prompt, collectAssistantText, forward, onPlanUpdate) {
33628
+ const args2 = ["acpx", "--format", "json", "--json-strict", "--approve-all", "--ttl", "0"];
33629
+ if (cwd)
33630
+ args2.push("--cwd", cwd);
33631
+ args2.push(agentType);
33632
+ if (sessionName)
33633
+ args2.push("prompt", "-s", sessionName);
33634
+ else
33635
+ args2.push("prompt");
33636
+ args2.push(prompt);
33637
+ dlog(`[acpx] prompt: ${args2.slice(0, 10).join(" ")} ... (prompt len=${prompt.length})`);
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
+ }
33642
+ let stopReason = "end_turn";
33643
+ (async () => {
33644
+ try {
33645
+ const r = proc.stderr.getReader();
33646
+ const d = new TextDecoder;
33647
+ let sb = "";
33648
+ while (true) {
33649
+ const { value: value3, done: done9 } = await r.read();
33650
+ if (done9)
33651
+ break;
33652
+ sb += d.decode(value3, { stream: true });
33653
+ let nl;
33654
+ while ((nl = sb.indexOf(`
33655
+ `)) !== -1) {
33656
+ const ln = sb.slice(0, nl).trim();
33657
+ sb = sb.slice(nl + 1);
33658
+ if (ln)
33659
+ dlog(`[acpx stderr] ${ln.slice(0, 500)}`);
33660
+ }
33661
+ }
33662
+ } catch {}
33663
+ })();
33664
+ const fakeOpts = { agentType, prompt, cwd, sessionName, onEvent: () => {}, onPlanUpdate };
33665
+ const reader = proc.stdout.getReader();
33666
+ const dec = new TextDecoder;
33667
+ let buf = "";
33668
+ while (true) {
33669
+ const { value: value3, done: done9 } = await reader.read();
33670
+ if (done9)
33671
+ break;
33672
+ buf += dec.decode(value3, { stream: true });
33673
+ let nl;
33674
+ while ((nl = buf.indexOf(`
33675
+ `)) !== -1) {
33676
+ const line = buf.slice(0, nl).trim();
33677
+ buf = buf.slice(nl + 1);
33678
+ if (!line)
33679
+ continue;
33680
+ dlog(`[acpx stdout] ${line.slice(0, 500)}`);
33681
+ const events = parseAcpLine(line, (r) => {
33682
+ stopReason = r;
33683
+ }, fakeOpts);
33684
+ for (const ev of events) {
33685
+ if (collectAssistantText && ev.event_type === "assistant_text") {
33686
+ const t = ev.payload?.text;
33687
+ if (typeof t === "string")
33688
+ collectAssistantText.buf += t;
33689
+ } else {
33690
+ await forward(ev);
33691
+ }
33692
+ }
33693
+ }
33694
+ }
33695
+ if (buf.trim()) {
33696
+ const events = parseAcpLine(buf.trim(), (r) => {
33697
+ stopReason = r;
33698
+ }, fakeOpts);
33699
+ for (const ev of events) {
33700
+ if (collectAssistantText && ev.event_type === "assistant_text") {
33701
+ const t = ev.payload?.text;
33702
+ if (typeof t === "string")
33703
+ collectAssistantText.buf += t;
33704
+ } else {
33705
+ await forward(ev);
33706
+ }
33707
+ }
33708
+ }
33709
+ const code = await proc.exited;
33710
+ if (typeof proc.pid === "number")
33711
+ removeAdapterPidfile(proc.pid);
33712
+ if (code !== 0 && code !== 130) {
33713
+ if (!collectAssistantText)
33714
+ await forward({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
33715
+ else
33716
+ dlog(`[acpx] summary spawn exit=${code}`);
33717
+ }
33718
+ return { stopReason };
33719
+ }
33467
33720
  async function runAcpx(opts) {
33468
33721
  const args2 = ["acpx", "--format", "json", "--json-strict", "--approve-all", "--ttl", "0"];
33469
33722
  if (opts.cwd)
@@ -33491,6 +33744,9 @@ async function runAcpx(opts) {
33491
33744
  try {
33492
33745
  opts.onSpawn?.(proc);
33493
33746
  } catch {}
33747
+ if (typeof proc.pid === "number") {
33748
+ writeAdapterPidfile({ pid: proc.pid, bin: "acpx", kind: "acpx", session_id: opts.sessionName ?? null });
33749
+ }
33494
33750
  let stopReason = "end_turn";
33495
33751
  (async () => {
33496
33752
  try {
@@ -33547,10 +33803,22 @@ async function runAcpx(opts) {
33547
33803
  await opts.onEvent(ev);
33548
33804
  }
33549
33805
  const code = await proc.exited;
33806
+ if (typeof proc.pid === "number")
33807
+ removeAdapterPidfile(proc.pid);
33550
33808
  if (code !== 0 && code !== 130) {
33551
33809
  await opts.onEvent({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
33552
33810
  }
33553
- return { stopReason };
33811
+ let summaryText;
33812
+ if (opts.summaryPrompt && opts.sessionName && (code === 0 || code === 130)) {
33813
+ const collect = { buf: "" };
33814
+ try {
33815
+ await spawnAcpxPrompt(opts.agentType, opts.cwd, opts.sessionName, opts.summaryPrompt, collect, () => {}, opts.onPlanUpdate);
33816
+ summaryText = collect.buf.trim() || undefined;
33817
+ } catch (e) {
33818
+ dlog(`[acpx] summary error: ${String(e)}`);
33819
+ }
33820
+ }
33821
+ return { stopReason, summaryText };
33554
33822
  }
33555
33823
  function parseAcpLine(line, setStop, opts) {
33556
33824
  let msg;
@@ -33652,8 +33920,9 @@ function extractText2(content) {
33652
33920
  }
33653
33921
  var HOME3, LOG_PATH2;
33654
33922
  var init_acpx_runner = __esm(() => {
33923
+ init_adapter_pidfile();
33655
33924
  HOME3 = process.env.HOME || process.env.USERPROFILE || ".";
33656
- LOG_PATH2 = join8(HOME3, ".multi", "logs", "agent.log");
33925
+ LOG_PATH2 = join9(HOME3, ".multi", "logs", "agent.log");
33657
33926
  });
33658
33927
 
33659
33928
  // ../../node_modules/zod/index.js
@@ -33879,12 +34148,12 @@ function parsePlanBlocks(text) {
33879
34148
  }
33880
34149
  return { actions, errors: errors3 };
33881
34150
  }
33882
- var PLAN_SCHEMA_VERSION = 5, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34151
+ var PLAN_SCHEMA_VERSION = 6, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
33883
34152
  var init_plans = __esm(() => {
33884
34153
  init_zod();
33885
34154
  Priority = exports_external.enum(["low", "medium", "high"]);
33886
34155
  AssigneeType = exports_external.enum(["human", "agent"]);
33887
- 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"]);
33888
34157
  SessionRole = exports_external.enum(["implementer", "reviewer", "test-fixer"]);
33889
34158
  SkillFile = exports_external.object({ path: exports_external.string().min(1), content: exports_external.string() });
33890
34159
  EvalPolicy = exports_external.object({
@@ -34085,7 +34354,6 @@ var init_chat = __esm(() => {
34085
34354
  runtime: exports_external.string().nullable().optional()
34086
34355
  });
34087
34356
  });
34088
-
34089
34357
  // ../lib/index.ts
34090
34358
  var init_lib = __esm(() => {
34091
34359
  init_streams();
@@ -34097,8 +34365,8 @@ var init_lib = __esm(() => {
34097
34365
 
34098
34366
  // src/_impl/git-enforce.ts
34099
34367
  import { spawn as spawn2 } from "node:child_process";
34100
- import { existsSync as existsSync9 } from "node:fs";
34101
- import { join as join9 } from "node:path";
34368
+ import { existsSync as existsSync10 } from "node:fs";
34369
+ import { join as join10 } from "node:path";
34102
34370
  function run4(cwd, cmd, args2) {
34103
34371
  return new Promise((resolve2) => {
34104
34372
  const p = spawn2(cmd, args2, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -34118,7 +34386,7 @@ function normalizeKey2(issueKey) {
34118
34386
  return issueKey.toLowerCase().replace(/[^a-z0-9\-_\/]/g, "-");
34119
34387
  }
34120
34388
  function worktreePath(baseWorkingDir, issueKey) {
34121
- return join9(baseWorkingDir, ".multi", "worktrees", normalizeKey2(issueKey));
34389
+ return join10(baseWorkingDir, ".multi", "worktrees", normalizeKey2(issueKey));
34122
34390
  }
34123
34391
  function branchFor(issueKey) {
34124
34392
  return `multi/${normalizeKey2(issueKey)}`;
@@ -34137,7 +34405,7 @@ function enforceCommitAndPush(baseWorkingDir, issueKey, mode = "enforce") {
34137
34405
  };
34138
34406
  if (mode === "off")
34139
34407
  return result;
34140
- if (!existsSync9(wt))
34408
+ if (!existsSync10(wt))
34141
34409
  return result;
34142
34410
  if (!(yield* isGitRepo2(wt)))
34143
34411
  return result;
@@ -34225,7 +34493,7 @@ var git = (cwd, args2, op) => exports_Effect.tryPromise({
34225
34493
  try: () => run4(cwd, "git", args2),
34226
34494
  catch: (cause3) => new GitError({ op, message: `git ${args2.join(" ")} threw`, cause: cause3 })
34227
34495
  }), isGitRepo2 = (dir) => exports_Effect.gen(function* () {
34228
- if (!existsSync9(dir))
34496
+ if (!existsSync10(dir))
34229
34497
  return false;
34230
34498
  const r = yield* git(dir, ["rev-parse", "--is-inside-work-tree"], "rev-parse");
34231
34499
  return r.code === 0 && r.stdout === "true";
@@ -34237,14 +34505,14 @@ var init_git_enforce = __esm(() => {
34237
34505
 
34238
34506
  // src/_impl/outbox.ts
34239
34507
  import { Database as Database2 } from "bun:sqlite";
34240
- import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
34241
- import { homedir } from "os";
34242
- import { join as join10 } from "path";
34508
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
34509
+ import { homedir as homedir2 } from "os";
34510
+ import { join as join11 } from "path";
34243
34511
  function ensureDb() {
34244
34512
  if (db)
34245
34513
  return db;
34246
- if (!existsSync10(MULTI_DIR3))
34247
- mkdirSync7(MULTI_DIR3, { recursive: true });
34514
+ if (!existsSync11(MULTI_DIR3))
34515
+ mkdirSync8(MULTI_DIR3, { recursive: true });
34248
34516
  db = new Database2(TASKS_DB_PATH2);
34249
34517
  db.exec(`
34250
34518
  CREATE TABLE IF NOT EXISTS stream_outbox (
@@ -34377,17 +34645,17 @@ function startOutboxFlusher(apiUrl, wsId) {
34377
34645
  var MULTI_DIR3, TASKS_DB_PATH2, BATCH_SIZE = 100, FLUSH_INTERVAL_MS = 2000, MAX_BACKOFF_MS = 30000, db = null;
34378
34646
  var init_outbox = __esm(() => {
34379
34647
  init_client();
34380
- MULTI_DIR3 = join10(homedir(), ".multi");
34381
- TASKS_DB_PATH2 = join10(MULTI_DIR3, "tasks.db");
34648
+ MULTI_DIR3 = join11(homedir2(), ".multi");
34649
+ TASKS_DB_PATH2 = join11(MULTI_DIR3, "tasks.db");
34382
34650
  });
34383
34651
 
34384
34652
  // src/_impl/run-task.ts
34385
- 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";
34386
- import { join as join11, dirname as dirname9 } from "path";
34653
+ 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";
34654
+ import { join as join12, dirname as dirname9 } from "path";
34387
34655
  function ensureDirs() {
34388
- for (const d of [MULTI_DIR4, join11(MULTI_DIR4, "logs"), SKILLS_DIR2]) {
34389
- if (!existsSync11(d))
34390
- mkdirSync8(d, { recursive: true });
34656
+ for (const d of [MULTI_DIR4, join12(MULTI_DIR4, "logs"), SKILLS_DIR2]) {
34657
+ if (!existsSync12(d))
34658
+ mkdirSync9(d, { recursive: true });
34391
34659
  }
34392
34660
  }
34393
34661
  function log3(msg) {
@@ -34431,10 +34699,42 @@ function resolveEnforcementMode(task) {
34431
34699
  return m;
34432
34700
  return "enforce";
34433
34701
  }
34702
+ function buildMemorySummaryPrompt(task) {
34703
+ const key = task?.key || task?.issue_id || "task";
34704
+ const title = task?.title || "";
34705
+ return [
34706
+ `The task "${key}: ${title}" is now complete.`,
34707
+ `Provide a memory summary for future agents working on this project.`,
34708
+ `Output EXACTLY 5 short bullets (one line each, prefix "- "):`,
34709
+ `1. What changed (the actual outcome)`,
34710
+ `2. Why (motivation, constraints)`,
34711
+ `3. Key decisions / tradeoffs`,
34712
+ `4. Files or modules touched`,
34713
+ `5. Open follow-ups, if any`,
34714
+ ``,
34715
+ `Rules: do NOT call any tools. Reply with ONLY the 5 bullets, no preamble, no closing.`
34716
+ ].join(`
34717
+ `);
34718
+ }
34719
+ async function writeTaskMemory(apiUrl, task, text) {
34720
+ if (!task?.project_id || !text)
34721
+ return;
34722
+ try {
34723
+ await apiClient.post(`${apiUrl}/api/memory/write`, {
34724
+ project_id: task.project_id,
34725
+ source_kind: "task",
34726
+ source_id: String(task.issue_id || task.id || ""),
34727
+ agent_id: task.agent_id ?? null,
34728
+ text: text.slice(0, 19500)
34729
+ });
34730
+ } catch (e) {
34731
+ log3(`memory write failed: ${String(e)}`);
34732
+ }
34733
+ }
34434
34734
  async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34435
34735
  const issueId = task.issue_id;
34436
34736
  const isFollowup = !!task.followup;
34437
- const baseWorkingDir = task.working_dir && existsSync11(task.working_dir) ? task.working_dir : undefined;
34737
+ const baseWorkingDir = task.working_dir && existsSync12(task.working_dir) ? task.working_dir : undefined;
34438
34738
  const tenantWsId = task.tenant_workspace_id ?? null;
34439
34739
  const projectId = task.project_id ?? null;
34440
34740
  const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
@@ -34501,18 +34801,19 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34501
34801
  await postStream(apiUrl, issueId, "progress", { message: `Device ${deviceId} picked up ${isFollowup ? "follow-up" : "task"}` });
34502
34802
  let attachmentRefs = [];
34503
34803
  if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
34504
- const baseDir = workingDir || join11(MULTI_DIR4, "tmp", issueId);
34505
- const inDir = join11(baseDir, ".multi-in", task.from_comment_id);
34804
+ const baseDir = workingDir || join12(MULTI_DIR4, "tmp", issueId);
34805
+ const inDir = join12(baseDir, ".multi-in", task.from_comment_id);
34506
34806
  attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
34507
34807
  if (attachmentRefs.length)
34508
34808
  log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
34509
34809
  }
34510
- const outDir = join11(workingDir || join11(MULTI_DIR4, "tmp", issueId), ".multi-out");
34810
+ const outDir = join12(workingDir || join12(MULTI_DIR4, "tmp", issueId), ".multi-out");
34511
34811
  let liveCommentId;
34512
34812
  let liveBody = "";
34513
34813
  let hadError = false;
34514
34814
  let hasAssistantText = false;
34515
34815
  let liveCommentPromise = null;
34816
+ let memorySummary = null;
34516
34817
  const ensureLiveComment = () => {
34517
34818
  if (liveCommentId)
34518
34819
  return Promise.resolve();
@@ -34821,7 +35122,7 @@ ${userPart}` : userPart;
34821
35122
  throw new Error(`ACP adapter for ${chosen.type} not found`);
34822
35123
  log3(` adapter: ${chosen.type} → ${adapterBin.join(" ")}`);
34823
35124
  const startedAt = Date.now();
34824
- const { sessionId } = await runAcp({
35125
+ const { sessionId, summaryText: acpSummary } = await runAcp({
34825
35126
  apiUrl,
34826
35127
  issueId,
34827
35128
  deviceId,
@@ -34833,6 +35134,7 @@ ${userPart}` : userPart;
34833
35134
  adapterBin,
34834
35135
  autonomy: task.autonomy_level,
34835
35136
  cwd: workingDir,
35137
+ summaryPrompt: buildMemorySummaryPrompt(task),
34836
35138
  onEvent: eventHandler,
34837
35139
  onSession: async (sid) => {
34838
35140
  try {
@@ -34857,6 +35159,7 @@ ${userPart}` : userPart;
34857
35159
  });
34858
35160
  postStream(apiUrl, issueId, "run_finished", { stopReason: typeof sessionId === "string" ? "ok" : "unknown", duration_ms: Date.now() - startedAt });
34859
35161
  log3(` acp session ${sessionId.slice(0, 8)}`);
35162
+ memorySummary = acpSummary || null;
34860
35163
  } else if (useAcpx) {
34861
35164
  let preamble = "";
34862
35165
  try {
@@ -34926,11 +35229,12 @@ Write generated files to: ${outDir}`;
34926
35229
  ${userPart}` : userPart;
34927
35230
  log3(` acpx runner: ${preferType}`);
34928
35231
  const acpxStartedAt = Date.now();
34929
- await runAcpx({
35232
+ const { summaryText: acpxSummary } = await runAcpx({
34930
35233
  agentType: preferType,
34931
35234
  prompt: full,
34932
35235
  cwd: workingDir,
34933
35236
  sessionName: `issue-${issueId}`,
35237
+ summaryPrompt: buildMemorySummaryPrompt(task),
34934
35238
  onEvent: eventHandler,
34935
35239
  onSpawn: (child) => {
34936
35240
  if (ctx?.runEntry) {
@@ -34941,6 +35245,7 @@ ${userPart}` : userPart;
34941
35245
  }
34942
35246
  });
34943
35247
  postStream(apiUrl, issueId, "run_finished", { stopReason: "ok", duration_ms: Date.now() - acpxStartedAt });
35248
+ memorySummary = acpxSummary || null;
34944
35249
  } else {
34945
35250
  const runner = pickRunner(detected, preferType);
34946
35251
  for await (const event of runner(task))
@@ -34984,6 +35289,10 @@ ${userPart}` : userPart;
34984
35289
  if (ISSUE_BASE)
34985
35290
  await apiClient.post(`${ISSUE_BASE}/complete`, {});
34986
35291
  log3(` ✓ ${task.key} complete`);
35292
+ if (memorySummary) {
35293
+ await writeTaskMemory(apiUrl, task, memorySummary);
35294
+ log3(` \uD83D\uDCDD memory summary written (${memorySummary.length} chars)`);
35295
+ }
34987
35296
  if (baseWorkingDir) {
34988
35297
  const mergeTarget = task.merge_to_main === true || task.merge_to_main === "true" ? "main" : typeof task.merge_target === "string" ? task.merge_target : null;
34989
35298
  await exports_Effect.runPromise(terminalGitFlowE(apiUrl, issueId, baseWorkingDir, task.key || issueId, resolveEnforcementMode(task), mergeTarget));
@@ -35008,7 +35317,12 @@ async function buildPlanningPreamble(apiUrl, task, _wsId) {
35008
35317
  if (depth >= PLANNING_DEPTH_LIMIT) {
35009
35318
  return `# Planning (sub-task context)
35010
35319
 
35011
- 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.
35320
+ 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.
35321
+
35322
+ Status guidance:
35323
+ - \`done\` — work is finished; no further human or agent action needed.
35324
+ - \`blocked\` — you need a human decision before continuing (confirmation, choice between approaches, missing info). Do NOT mark \`done\` when waiting on review.
35325
+ - \`failed\` — work could not be completed; explain in your reply.
35012
35326
 
35013
35327
  \`\`\`multi-plan
35014
35328
  {"actions":[{"type":"update","id":"<this issue id>","status":"done"}]}
@@ -35056,6 +35370,8 @@ Issue actions:
35056
35370
  ]}
35057
35371
  \`\`\`
35058
35372
 
35373
+ 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.
35374
+
35059
35375
  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.
35060
35376
 
35061
35377
  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).
@@ -35174,7 +35490,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35174
35490
  }
35175
35491
  lines.push(`- [ok] updated ${res.data.key}`);
35176
35492
  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 });
35177
- if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync11(parentTask.working_dir)) {
35493
+ if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync12(parentTask.working_dir)) {
35178
35494
  const targetKey = res.data?.key;
35179
35495
  if (targetKey) {
35180
35496
  const targetIssueId = res.data?.id || a.id;
@@ -35455,26 +35771,26 @@ function statusIcon(status3) {
35455
35771
  }
35456
35772
  }
35457
35773
  async function resolveAcpAdapter(agentType, detectedPath) {
35458
- if (agentType === "pi" && detectedPath && existsSync11(detectedPath)) {
35774
+ if (agentType === "pi" && detectedPath && existsSync12(detectedPath)) {
35459
35775
  return [detectedPath, "--mode", "rpc"];
35460
35776
  }
35461
35777
  const override = process.env.MULTI_ACP_ADAPTER?.trim();
35462
35778
  const adapterNames = override ? [override] : ["claude-code-acp", "claude-agent-acp"];
35463
35779
  for (const name of adapterNames) {
35464
- if (name.startsWith("/") && existsSync11(name))
35780
+ if (name.startsWith("/") && existsSync12(name))
35465
35781
  return [name];
35466
35782
  try {
35467
35783
  const here = new URL(import.meta.url).pathname;
35468
35784
  let dir = here;
35469
35785
  for (let i = 0;i < 8; i++) {
35470
35786
  dir = dirname9(dir);
35471
- const bin = join11(dir, "node_modules", ".bin", name);
35472
- if (existsSync11(bin))
35787
+ const bin = join12(dir, "node_modules", ".bin", name);
35788
+ if (existsSync12(bin))
35473
35789
  return [bin];
35474
35790
  }
35475
35791
  } catch {}
35476
- const global = join11(HOME4, ".bun", "install", "global", "node_modules", ".bin", name);
35477
- if (existsSync11(global))
35792
+ const global = join12(HOME4, ".bun", "install", "global", "node_modules", ".bin", name);
35793
+ if (existsSync12(global))
35478
35794
  return [global];
35479
35795
  }
35480
35796
  return null;
@@ -35483,7 +35799,7 @@ async function postStream(_apiUrl, issueId, event_type, payload) {
35483
35799
  try {
35484
35800
  ensureDirs();
35485
35801
  const date6 = new Date().toISOString().slice(0, 10);
35486
- const path = join11(MULTI_DIR4, "logs", `events-${date6}.ndjson`);
35802
+ const path = join12(MULTI_DIR4, "logs", `events-${date6}.ndjson`);
35487
35803
  appendFileSync4(path, JSON.stringify({ ts: Date.now(), issue_id: issueId, event_type, payload }) + `
35488
35804
  `);
35489
35805
  } catch {}
@@ -35502,7 +35818,7 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
35502
35818
  const items = list.data?.results || list.data || [];
35503
35819
  if (!Array.isArray(items) || items.length === 0)
35504
35820
  return [];
35505
- mkdirSync8(destDir, { recursive: true });
35821
+ mkdirSync9(destDir, { recursive: true });
35506
35822
  const token = authTokenHeader();
35507
35823
  const out = [];
35508
35824
  for (const it of items) {
@@ -35511,8 +35827,8 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
35511
35827
  continue;
35512
35828
  const buf = new Uint8Array(await res.arrayBuffer());
35513
35829
  const safe = it.filename.replace(/[^A-Za-z0-9._-]/g, "_");
35514
- const p = join11(destDir, safe);
35515
- writeFileSync6(p, buf);
35830
+ const p = join12(destDir, safe);
35831
+ writeFileSync7(p, buf);
35516
35832
  out.push({ filename: it.filename, path: p });
35517
35833
  }
35518
35834
  return out;
@@ -35522,9 +35838,9 @@ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, comm
35522
35838
  }
35523
35839
  function authTokenHeader() {
35524
35840
  try {
35525
- if (!existsSync11(CONFIG_PATH2))
35841
+ if (!existsSync12(CONFIG_PATH2))
35526
35842
  return null;
35527
- const raw = JSON.parse(readFileSync9(CONFIG_PATH2, "utf8"));
35843
+ const raw = JSON.parse(readFileSync10(CONFIG_PATH2, "utf8"));
35528
35844
  const token = raw.token ?? raw.authToken;
35529
35845
  return token ? `Bearer ${token}` : null;
35530
35846
  } catch {
@@ -35532,14 +35848,14 @@ function authTokenHeader() {
35532
35848
  }
35533
35849
  }
35534
35850
  async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
35535
- if (!existsSync11(dir))
35851
+ if (!existsSync12(dir))
35536
35852
  return 0;
35537
35853
  const files = [];
35538
35854
  const walk = (d, depth = 0) => {
35539
35855
  if (depth > 3)
35540
35856
  return;
35541
- for (const name of readdirSync2(d)) {
35542
- const p = join11(d, name);
35857
+ for (const name of readdirSync3(d)) {
35858
+ const p = join12(d, name);
35543
35859
  try {
35544
35860
  const st = statSync2(p);
35545
35861
  if (st.isDirectory())
@@ -35557,7 +35873,7 @@ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir)
35557
35873
  let uploaded = 0;
35558
35874
  for (const f of files) {
35559
35875
  try {
35560
- const data = readFileSync9(f);
35876
+ const data = readFileSync10(f);
35561
35877
  const form = new FormData;
35562
35878
  const blob = new Blob([data]);
35563
35879
  form.append("file", blob, f.split("/").pop() || "file");
@@ -35572,7 +35888,7 @@ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir)
35572
35888
  if (res.ok) {
35573
35889
  uploaded++;
35574
35890
  try {
35575
- unlinkSync5(f);
35891
+ unlinkSync6(f);
35576
35892
  } catch {}
35577
35893
  }
35578
35894
  } catch (e) {
@@ -35848,10 +36164,10 @@ var init_run_task = __esm(() => {
35848
36164
  init_git_enforce();
35849
36165
  init_outbox();
35850
36166
  HOME4 = process.env.HOME || process.env.USERPROFILE || ".";
35851
- MULTI_DIR4 = join11(HOME4, ".multi");
35852
- CONFIG_PATH2 = join11(MULTI_DIR4, "config.json");
35853
- LOG_PATH3 = join11(MULTI_DIR4, "logs", "agent.log");
35854
- SKILLS_DIR2 = join11(MULTI_DIR4, "skills");
36167
+ MULTI_DIR4 = join12(HOME4, ".multi");
36168
+ CONFIG_PATH2 = join12(MULTI_DIR4, "config.json");
36169
+ LOG_PATH3 = join12(MULTI_DIR4, "logs", "agent.log");
36170
+ SKILLS_DIR2 = join12(MULTI_DIR4, "skills");
35855
36171
  });
35856
36172
 
35857
36173
  // ../lib/chat-doc.ts
@@ -35897,12 +36213,12 @@ function patchMessage(doc2, mapId, patch9) {
35897
36213
  map20.set(k, v);
35898
36214
  }
35899
36215
  }
35900
- function appendText(doc2, mapId, chunk2) {
36216
+ function appendText(doc2, mapId, chunk3) {
35901
36217
  const map20 = doc2.getContainerById(mapId);
35902
36218
  if (!map20)
35903
36219
  return;
35904
36220
  const cur = map20.get("text") ?? "";
35905
- map20.set("text", cur + chunk2);
36221
+ map20.set("text", cur + chunk3);
35906
36222
  }
35907
36223
  function finalizeMessage(doc2, mapId) {
35908
36224
  const map20 = doc2.getContainerById(mapId);
@@ -35943,8 +36259,8 @@ var init_chat_doc = __esm(() => {
35943
36259
  });
35944
36260
 
35945
36261
  // src/_impl/chat-peer.ts
35946
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
35947
- import { dirname as dirname10, join as join12 } from "path";
36262
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
36263
+ import { dirname as dirname10, join as join13 } from "path";
35948
36264
  import { LoroDoc as LoroDoc2 } from "loro-crdt";
35949
36265
 
35950
36266
  class ChatPeer {
@@ -35961,24 +36277,24 @@ class ChatPeer {
35961
36277
  constructor(opts) {
35962
36278
  this.opts = opts;
35963
36279
  this.chatId = opts.chatId;
35964
- this.snapshotPath = join12(MULTI_DIR, "chats", `${opts.chatId}.loro`);
36280
+ this.snapshotPath = join13(MULTI_DIR, "chats", `${opts.chatId}.loro`);
35965
36281
  this.doc = this.loadFromDisk();
35966
36282
  for (const m of listMessages(this.doc))
35967
36283
  this.seenIds.add(m.id);
35968
36284
  }
35969
36285
  loadFromDisk() {
35970
36286
  try {
35971
- if (existsSync12(this.snapshotPath)) {
35972
- const bytes = readFileSync10(this.snapshotPath);
36287
+ if (existsSync13(this.snapshotPath)) {
36288
+ const bytes = readFileSync11(this.snapshotPath);
35973
36289
  return importSnapshot(new Uint8Array(bytes));
35974
36290
  }
35975
36291
  } catch {}
35976
36292
  return new LoroDoc2;
35977
36293
  }
35978
36294
  persist() {
35979
- if (!existsSync12(dirname10(this.snapshotPath)))
35980
- mkdirSync9(dirname10(this.snapshotPath), { recursive: true });
35981
- writeFileSync7(this.snapshotPath, exportSnapshot(this.doc));
36295
+ if (!existsSync13(dirname10(this.snapshotPath)))
36296
+ mkdirSync10(dirname10(this.snapshotPath), { recursive: true });
36297
+ writeFileSync8(this.snapshotPath, exportSnapshot(this.doc));
35982
36298
  this.dirtySinceWrite = 0;
35983
36299
  }
35984
36300
  appendAndPush(msg) {
@@ -36013,8 +36329,8 @@ class ChatPeer {
36013
36329
  this.flush();
36014
36330
  return { msgId: msg.id, containerId };
36015
36331
  }
36016
- appendPartialText(containerId, chunk2) {
36017
- appendText(this.doc, containerId, chunk2);
36332
+ appendPartialText(containerId, chunk3) {
36333
+ appendText(this.doc, containerId, chunk3);
36018
36334
  this.flush();
36019
36335
  }
36020
36336
  finalizePartialMessage(containerId) {
@@ -37024,6 +37340,7 @@ Action vocabulary:
37024
37340
 
37025
37341
  Rules:
37026
37342
  - Omit the block entirely if no actions are needed. Don't emit empty arrays.
37343
+ - 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.
37027
37344
  - 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.
37028
37345
  - 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.
37029
37346
  - 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.
@@ -37798,7 +38115,7 @@ import { parseArgs } from "util";
37798
38115
  // package.json
37799
38116
  var package_default = {
37800
38117
  name: "@shipers-dev/multi",
37801
- version: "0.48.0",
38118
+ version: "0.51.0",
37802
38119
  type: "module",
37803
38120
  bin: {
37804
38121
  "multi-agent": "./dist/index.js"
@@ -38312,7 +38629,7 @@ class Runners extends exports_Effect.Service()("cli/Runners", {
38312
38629
  // src/commands/simple.ts
38313
38630
  init_esm();
38314
38631
  init_paths();
38315
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
38632
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
38316
38633
 
38317
38634
  // src/services/Config.ts
38318
38635
  init_esm();
@@ -38407,7 +38724,7 @@ var statusCmd = exports_Effect.fn("statusCmd")(function* () {
38407
38724
  process.exit(1);
38408
38725
  }
38409
38726
  const d = res.data;
38410
- const pid = existsSync6(PID_PATH) ? readFileSync6(PID_PATH, "utf8").trim() : null;
38727
+ const pid = existsSync7(PID_PATH) ? readFileSync7(PID_PATH, "utf8").trim() : null;
38411
38728
  const daemon = pid && isRunning3(Number(pid)) ? `running (pid ${pid})` : "stopped";
38412
38729
  console.log(`
38413
38730
  Device Status
@@ -38422,11 +38739,11 @@ Daemon: ${daemon}
38422
38739
  });
38423
38740
  var stopCmd = exports_Effect.fn("stopCmd")(function* () {
38424
38741
  const fs3 = yield* FileSystem;
38425
- if (!existsSync6(PID_PATH)) {
38742
+ if (!existsSync7(PID_PATH)) {
38426
38743
  console.log("No daemon running.");
38427
38744
  return;
38428
38745
  }
38429
- const pid = Number(readFileSync6(PID_PATH, "utf8").trim());
38746
+ const pid = Number(readFileSync7(PID_PATH, "utf8").trim());
38430
38747
  if (!pid || !isRunning3(pid)) {
38431
38748
  yield* fs3.remove(PID_PATH);
38432
38749
  console.log("Cleaned stale pidfile.");
@@ -38439,11 +38756,11 @@ var stopCmd = exports_Effect.fn("stopCmd")(function* () {
38439
38756
  } catch {}
38440
38757
  });
38441
38758
  var logsCmd = exports_Effect.fn("logsCmd")(function* () {
38442
- if (!existsSync6(LOG_PATH)) {
38759
+ if (!existsSync7(LOG_PATH)) {
38443
38760
  console.log("No logs yet.");
38444
38761
  return;
38445
38762
  }
38446
- const content = readFileSync6(LOG_PATH, "utf8");
38763
+ const content = readFileSync7(LOG_PATH, "utf8");
38447
38764
  console.log(content.split(`
38448
38765
  `).slice(-100).join(`
38449
38766
  `));
@@ -38452,10 +38769,10 @@ var resetCmd = exports_Effect.fn("resetCmd")(function* (issueId) {
38452
38769
  if (!issueId) {
38453
38770
  return yield* exports_Effect.fail(new UsageError({ message: "reset requires --issue <id>" }));
38454
38771
  }
38455
- if (!existsSync6(PORT_PATH)) {
38772
+ if (!existsSync7(PORT_PATH)) {
38456
38773
  return yield* exports_Effect.fail(new DaemonError({ message: "Daemon not running (no port file)." }));
38457
38774
  }
38458
- const port = Number(readFileSync6(PORT_PATH, "utf8").trim());
38775
+ const port = Number(readFileSync7(PORT_PATH, "utf8").trim());
38459
38776
  const config2 = yield* Config4;
38460
38777
  const cfg = yield* config2.load;
38461
38778
  if (!cfg.dispatchSecret) {
@@ -38616,10 +38933,10 @@ var linkCmd = exports_Effect.fn("linkCmd")(function* (apiUrl, agentId) {
38616
38933
  // src/commands/restart.ts
38617
38934
  init_esm();
38618
38935
  init_paths();
38619
- import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync4 } from "fs";
38936
+ import { existsSync as existsSync9, readFileSync as readFileSync9, unlinkSync as unlinkSync5 } from "fs";
38620
38937
 
38621
38938
  // src/_impl/pid-lock.ts
38622
- import { closeSync, existsSync as existsSync7, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeSync } from "fs";
38939
+ import { closeSync, existsSync as existsSync8, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync8, unlinkSync as unlinkSync4, writeSync } from "fs";
38623
38940
  import { dirname as dirname8 } from "path";
38624
38941
  var isAlive = (pid) => {
38625
38942
  if (!Number.isFinite(pid) || pid <= 0)
@@ -38632,7 +38949,7 @@ var isAlive = (pid) => {
38632
38949
  }
38633
38950
  };
38634
38951
  var writeExclusive = (path, pid) => {
38635
- mkdirSync6(dirname8(path), { recursive: true });
38952
+ mkdirSync7(dirname8(path), { recursive: true });
38636
38953
  const fd = openSync(path, "wx");
38637
38954
  try {
38638
38955
  writeSync(fd, String(pid));
@@ -38651,7 +38968,7 @@ var acquirePidLock = (path) => {
38651
38968
  }
38652
38969
  const raw = (() => {
38653
38970
  try {
38654
- return readFileSync7(path, "utf8").trim();
38971
+ return readFileSync8(path, "utf8").trim();
38655
38972
  } catch {
38656
38973
  return "";
38657
38974
  }
@@ -38663,7 +38980,7 @@ var acquirePidLock = (path) => {
38663
38980
  return { ok: false, existingPid };
38664
38981
  }
38665
38982
  try {
38666
- unlinkSync3(path);
38983
+ unlinkSync4(path);
38667
38984
  } catch {}
38668
38985
  try {
38669
38986
  writeExclusive(path, ourPid);
@@ -38673,7 +38990,7 @@ var acquirePidLock = (path) => {
38673
38990
  throw e;
38674
38991
  const racingRaw = (() => {
38675
38992
  try {
38676
- return readFileSync7(path, "utf8").trim();
38993
+ return readFileSync8(path, "utf8").trim();
38677
38994
  } catch {
38678
38995
  return "";
38679
38996
  }
@@ -38684,12 +39001,12 @@ var acquirePidLock = (path) => {
38684
39001
  };
38685
39002
  var releasePidLock = (path) => {
38686
39003
  try {
38687
- if (!existsSync7(path))
39004
+ if (!existsSync8(path))
38688
39005
  return;
38689
- const raw = readFileSync7(path, "utf8").trim();
39006
+ const raw = readFileSync8(path, "utf8").trim();
38690
39007
  if (Number(raw) !== process.pid)
38691
39008
  return;
38692
- unlinkSync3(path);
39009
+ unlinkSync4(path);
38693
39010
  } catch {}
38694
39011
  };
38695
39012
  var killStaleConnects = () => {
@@ -38754,8 +39071,8 @@ var isRunning4 = (pid) => {
38754
39071
  var restartCmd = exports_Effect.fn("restartCmd")(function* (apiUrl) {
38755
39072
  const fs3 = yield* FileSystem;
38756
39073
  yield* (yield* Config4).ensureDirs;
38757
- if (existsSync8(PID_PATH)) {
38758
- const pid = Number(readFileSync8(PID_PATH, "utf8").trim());
39074
+ if (existsSync9(PID_PATH)) {
39075
+ const pid = Number(readFileSync9(PID_PATH, "utf8").trim());
38759
39076
  if (pid && isRunning4(pid)) {
38760
39077
  yield* fs3.writeText(STOP_PATH, "1");
38761
39078
  try {
@@ -38774,13 +39091,13 @@ var restartCmd = exports_Effect.fn("restartCmd")(function* (apiUrl) {
38774
39091
  }
38775
39092
  }
38776
39093
  try {
38777
- if (existsSync8(PID_PATH))
38778
- unlinkSync4(PID_PATH);
39094
+ if (existsSync9(PID_PATH))
39095
+ unlinkSync5(PID_PATH);
38779
39096
  } catch {}
38780
39097
  }
38781
39098
  try {
38782
- if (existsSync8(STOP_PATH))
38783
- unlinkSync4(STOP_PATH);
39099
+ if (existsSync9(STOP_PATH))
39100
+ unlinkSync5(STOP_PATH);
38784
39101
  } catch {}
38785
39102
  const killed = killStaleConnects();
38786
39103
  if (killed.length)
@@ -38876,21 +39193,22 @@ init_client();
38876
39193
  init_detect();
38877
39194
  init_run_task();
38878
39195
  import { Database as Database3 } from "bun:sqlite";
38879
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6 } from "fs";
38880
- import { homedir as homedir2 } from "os";
38881
- import { join as join13 } from "path";
39196
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync7 } from "fs";
39197
+ import { homedir as homedir3 } from "os";
39198
+ import { join as join14 } from "path";
39199
+ init_adapter_pidfile();
38882
39200
  init_errors();
38883
- var MULTI_DIR5 = join13(homedir2(), ".multi");
38884
- var PID_PATH2 = join13(MULTI_DIR5, "agent.pid");
38885
- var PORT_PATH2 = join13(MULTI_DIR5, "agent.port");
38886
- var STOP_PATH2 = join13(MULTI_DIR5, "stop.flag");
38887
- var TASKS_DB_PATH3 = join13(MULTI_DIR5, "tasks.db");
39201
+ var MULTI_DIR5 = join14(homedir3(), ".multi");
39202
+ var PID_PATH2 = join14(MULTI_DIR5, "agent.pid");
39203
+ var PORT_PATH2 = join14(MULTI_DIR5, "agent.port");
39204
+ var STOP_PATH2 = join14(MULTI_DIR5, "stop.flag");
39205
+ var TASKS_DB_PATH3 = join14(MULTI_DIR5, "tasks.db");
38888
39206
  function ensureDirs2() {
38889
- if (!existsSync13(MULTI_DIR5))
38890
- mkdirSync10(MULTI_DIR5, { recursive: true });
38891
- const logs = join13(MULTI_DIR5, "logs");
38892
- if (!existsSync13(logs))
38893
- mkdirSync10(logs, { recursive: true });
39207
+ if (!existsSync14(MULTI_DIR5))
39208
+ mkdirSync11(MULTI_DIR5, { recursive: true });
39209
+ const logs = join14(MULTI_DIR5, "logs");
39210
+ if (!existsSync14(logs))
39211
+ mkdirSync11(logs, { recursive: true });
38894
39212
  }
38895
39213
  function isLocalApi(url2) {
38896
39214
  try {
@@ -39182,8 +39500,11 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39182
39500
  if (cfg.authToken)
39183
39501
  setAuthToken(cfg.authToken);
39184
39502
  ensureDirs2();
39185
- if (existsSync13(STOP_PATH2))
39186
- unlinkSync6(STOP_PATH2);
39503
+ if (existsSync14(STOP_PATH2))
39504
+ unlinkSync7(STOP_PATH2);
39505
+ const reaped = reapStaleAdapters((m) => log3(m));
39506
+ if (reaped > 0)
39507
+ log3(`reaped ${reaped} stale adapter${reaped === 1 ? "" : "s"}`);
39187
39508
  const detected = yield* exports_Effect.promise(() => detectAgents());
39188
39509
  const localMode = isLocalApi(apiUrl);
39189
39510
  log3(`daemon device=${cfg.deviceId} pid=${process.pid}`);
@@ -39317,10 +39638,10 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39317
39638
  });
39318
39639
  log3(`Local server: http://127.0.0.1:${port}`);
39319
39640
  try {
39320
- writeFileSync8(PORT_PATH2, String(port));
39641
+ writeFileSync9(PORT_PATH2, String(port));
39321
39642
  } catch {}
39322
39643
  try {
39323
- writeFileSync8(PID_PATH2, String(process.pid));
39644
+ writeFileSync9(PID_PATH2, String(process.pid));
39324
39645
  } catch {}
39325
39646
  let tunnel;
39326
39647
  if (localMode) {
@@ -39384,7 +39705,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39384
39705
  let probeFailures = 0;
39385
39706
  while (true) {
39386
39707
  yield* exports_Effect.sleep(exports_Duration.seconds(120));
39387
- if (existsSync13(STOP_PATH2)) {
39708
+ if (existsSync14(STOP_PATH2)) {
39388
39709
  yield* exports_Deferred.succeed(stopDeferred, "stop flag");
39389
39710
  return;
39390
39711
  }
@@ -39409,7 +39730,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39409
39730
  yield* exports_Effect.forkIn(daemonScope)(exports_Effect.gen(function* () {
39410
39731
  while (true) {
39411
39732
  yield* exports_Effect.sleep(exports_Duration.seconds(5));
39412
- if (existsSync13(STOP_PATH2)) {
39733
+ if (existsSync14(STOP_PATH2)) {
39413
39734
  yield* exports_Deferred.succeed(stopDeferred, "stop flag");
39414
39735
  return;
39415
39736
  }
@@ -39449,12 +39770,12 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
39449
39770
  yield* exports_Effect.race(exports_Fiber.interruptAll(inFlight), exports_Effect.sleep(exports_Duration.seconds(10)));
39450
39771
  }
39451
39772
  yield* exports_Scope.close(daemonScope, exports_Exit.void).pipe(exports_Effect.catchAll(() => exports_Effect.void));
39452
- if (existsSync13(PID_PATH2))
39453
- unlinkSync6(PID_PATH2);
39454
- if (existsSync13(STOP_PATH2))
39455
- unlinkSync6(STOP_PATH2);
39456
- if (existsSync13(PORT_PATH2))
39457
- unlinkSync6(PORT_PATH2);
39773
+ if (existsSync14(PID_PATH2))
39774
+ unlinkSync7(PID_PATH2);
39775
+ if (existsSync14(STOP_PATH2))
39776
+ unlinkSync7(STOP_PATH2);
39777
+ if (existsSync14(PORT_PATH2))
39778
+ unlinkSync7(PORT_PATH2);
39458
39779
  db2.close();
39459
39780
  log3("disconnected");
39460
39781
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.48.0",
3
+ "version": "0.51.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"