@shipers-dev/multi 0.25.0 → 0.32.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 +335 -209
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -15001,7 +15001,7 @@ import { parseArgs } from "util";
15001
15001
  // package.json
15002
15002
  var package_default = {
15003
15003
  name: "@shipers-dev/multi",
15004
- version: "0.25.0",
15004
+ version: "0.32.0",
15005
15005
  type: "module",
15006
15006
  bin: {
15007
15007
  "multi-agent": "./dist/index.js"
@@ -15705,8 +15705,12 @@ multi-managed: true
15705
15705
  `;
15706
15706
  writeFileSync4(out, fm + (agent.prompt || ""));
15707
15707
  }
15708
- async function materializeBundle(apiUrl, deviceId, log3) {
15709
- const res = await apiClient.get(`${apiUrl}/api/devices/${deviceId}/agent_bundle`);
15708
+ async function materializeBundle(apiUrl, wsId, deviceId, log3) {
15709
+ if (!wsId) {
15710
+ log3("materialize: skipped (no workspace id)");
15711
+ return null;
15712
+ }
15713
+ const res = await apiClient.get(`${apiUrl}/api/workspaces/${wsId}/devices/${deviceId}/agent_bundle`);
15710
15714
  if (!res.success || !res.data) {
15711
15715
  log3(`materialize: bundle fetch failed: ${res.error || "unknown"}`);
15712
15716
  return null;
@@ -15775,11 +15779,11 @@ class Materializer extends exports_Effect.Service()("cli/Materializer", {
15775
15779
  effect: exports_Effect.gen(function* () {
15776
15780
  const logger = yield* Logger4;
15777
15781
  const sync5 = (f = () => {}) => f;
15778
- const materialize = (apiUrl, deviceId) => exports_Effect.tryPromise({
15779
- try: () => materializeBundle(apiUrl, deviceId, (m) => {
15782
+ const materialize = (apiUrl, wsId, deviceId) => exports_Effect.tryPromise({
15783
+ try: () => materializeBundle(apiUrl, wsId, deviceId, (m) => {
15780
15784
  exports_Effect.runPromise(logger.log(m));
15781
15785
  }),
15782
- catch: (cause3) => new ApiError({ url: `${apiUrl}/api/devices/${deviceId}/agent_bundle`, message: String(cause3) })
15786
+ catch: (cause3) => new ApiError({ url: `${apiUrl}/api/workspaces/${wsId}/devices/${deviceId}/agent_bundle`, message: String(cause3) })
15783
15787
  });
15784
15788
  const lastRevision = () => exports_Effect.sync(() => lastMaterializedRevision());
15785
15789
  return { materialize, lastRevision };
@@ -31543,7 +31547,10 @@ ${entries2}` } });
31543
31547
  if (allowOpt)
31544
31548
  return { outcome: { outcome: "selected", optionId: allowOpt.optionId } };
31545
31549
  }
31546
- const created = await apiClient.post(`${o.apiUrl}/api/permissions`, {
31550
+ if (!o.tenantWsId)
31551
+ return { outcome: { outcome: "cancelled" } };
31552
+ const permBase = `${o.apiUrl}/api/workspaces/${o.tenantWsId}/permissions`;
31553
+ const created = await apiClient.post(permBase, {
31547
31554
  issue_id: o.issueId,
31548
31555
  device_id: o.deviceId,
31549
31556
  tool_call_id: tc.id || tc.toolCallId,
@@ -31557,7 +31564,7 @@ ${entries2}` } });
31557
31564
  const deadline = Date.now() + 5 * 60 * 1000;
31558
31565
  while (Date.now() < deadline) {
31559
31566
  await new Promise((r) => setTimeout(r, 500));
31560
- const got = await apiClient.get(`${o.apiUrl}/api/permissions/${permId}`);
31567
+ const got = await apiClient.get(`${permBase}/${permId}`);
31561
31568
  const row = got.data;
31562
31569
  if (!row)
31563
31570
  break;
@@ -31867,7 +31874,11 @@ var statusCmd = exports_Effect.fn("statusCmd")(function* () {
31867
31874
  const apiUrl = cfg.apiUrl || "https://multi-api.adnb3r.workers.dev";
31868
31875
  if (cfg.authToken)
31869
31876
  yield* api2.setAuthToken(cfg.authToken);
31870
- const res = yield* api2.get(`${apiUrl}/api/devices/${cfg.deviceId}`);
31877
+ if (!cfg.workspaceId) {
31878
+ console.log("❌ Device has no workspace id. Re-run setup.");
31879
+ process.exit(1);
31880
+ }
31881
+ const res = yield* api2.get(`${apiUrl}/api/workspaces/${cfg.workspaceId}/devices/${cfg.deviceId}`);
31871
31882
  if (!res.success) {
31872
31883
  console.log("❌", res.error);
31873
31884
  process.exit(1);
@@ -31960,11 +31971,11 @@ class Materializer2 extends exports_Effect.Service()("cli/Materializer", {
31960
31971
  effect: exports_Effect.gen(function* () {
31961
31972
  const logger = yield* Logger4;
31962
31973
  const sync5 = (f = () => {}) => f;
31963
- const materialize = (apiUrl, deviceId) => exports_Effect.tryPromise({
31964
- try: () => materializeBundle(apiUrl, deviceId, (m) => {
31974
+ const materialize = (apiUrl, wsId, deviceId) => exports_Effect.tryPromise({
31975
+ try: () => materializeBundle(apiUrl, wsId, deviceId, (m) => {
31965
31976
  exports_Effect.runPromise(logger.log(m));
31966
31977
  }),
31967
- catch: (cause3) => new ApiError({ url: `${apiUrl}/api/devices/${deviceId}/agent_bundle`, message: String(cause3) })
31978
+ catch: (cause3) => new ApiError({ url: `${apiUrl}/api/workspaces/${wsId}/devices/${deviceId}/agent_bundle`, message: String(cause3) })
31968
31979
  });
31969
31980
  const lastRevision = () => exports_Effect.sync(() => lastMaterializedRevision());
31970
31981
  return { materialize, lastRevision };
@@ -32037,11 +32048,10 @@ var setupCmd = exports_Effect.fn("setupCmd")(function* (name, apiUrl) {
32037
32048
  ✅ Device paired. ID: ${a.device_id}`);
32038
32049
  yield* config2.save({ deviceId: a.device_id, authToken: a.token, dispatchSecret: a.dispatch_secret, apiUrl });
32039
32050
  yield* api2.setAuthToken(a.token);
32040
- const dev = yield* api2.get(`${apiUrl}/api/devices/${a.device_id}`);
32041
- const workspaceId = dev.data?.workspace_id;
32051
+ const workspaceId = a.workspace_id || undefined;
32042
32052
  if (workspaceId) {
32043
32053
  yield* config2.update({ workspaceId });
32044
- yield* materializer.materialize(apiUrl, a.device_id).pipe(exports_Effect.catchAll((e) => exports_Effect.sync(() => console.log(` materialize failed: ${e.message}`))));
32054
+ yield* materializer.materialize(apiUrl, workspaceId, a.device_id).pipe(exports_Effect.catchAll((e) => exports_Effect.sync(() => console.log(` materialize failed: ${e.message}`))));
32045
32055
  }
32046
32056
  console.log(`
32047
32057
  Next: link to an agent with: multi-agent link --agent <agentId>`);
@@ -32059,7 +32069,10 @@ var linkCmd = exports_Effect.fn("linkCmd")(function* (apiUrl, agentId) {
32059
32069
  }
32060
32070
  if (cfg.authToken)
32061
32071
  yield* api2.setAuthToken(cfg.authToken);
32062
- const url2 = `${apiUrl}/api/devices/${cfg.deviceId}/link`;
32072
+ if (!cfg.workspaceId) {
32073
+ return yield* exports_Effect.fail(new UsageError({ message: "Device has no workspace id. Re-run setup." }));
32074
+ }
32075
+ const url2 = `${apiUrl}/api/workspaces/${cfg.workspaceId}/devices/${cfg.deviceId}/link`;
32063
32076
  const res = yield* api2.post(url2, { agent_id: agentId });
32064
32077
  if (!res.success) {
32065
32078
  return yield* exports_Effect.fail(new ApiError({ url: url2, status: res.status, message: res.error || "link failed" }));
@@ -32264,7 +32277,9 @@ function bumpFailed(ids3, reason) {
32264
32277
  WHERE id = ?`, attempts, Date.now() + backoff, reason, id);
32265
32278
  }
32266
32279
  }
32267
- async function flushOutboxOnce(apiUrl) {
32280
+ async function flushOutboxOnce(apiUrl, wsId) {
32281
+ if (!wsId)
32282
+ return { attempted: 0, accepted: 0, rejected: 0 };
32268
32283
  const rows = pickPending(BATCH_SIZE);
32269
32284
  if (!rows.length)
32270
32285
  return { attempted: 0, accepted: 0, rejected: 0 };
@@ -32276,7 +32291,7 @@ async function flushOutboxOnce(apiUrl) {
32276
32291
  created_at: r.created_at
32277
32292
  }));
32278
32293
  try {
32279
- const res = await apiClient.post(`${apiUrl}/api/streams/ingest`, { events });
32294
+ const res = await apiClient.post(`${apiUrl}/api/workspaces/${wsId}/streams/ingest`, { events });
32280
32295
  if (!res.success || !res.data) {
32281
32296
  bumpFailed(rows.map((r) => r.id), res.error ?? "ingest_post_failed");
32282
32297
  return { attempted: rows.length, accepted: 0, rejected: rows.length };
@@ -32309,7 +32324,7 @@ function tryParse(s) {
32309
32324
  return s;
32310
32325
  }
32311
32326
  }
32312
- function startOutboxFlusher(apiUrl) {
32327
+ function startOutboxFlusher(apiUrl, wsId) {
32313
32328
  let stopped = false;
32314
32329
  let timer = null;
32315
32330
  const tick = async () => {
@@ -32317,7 +32332,7 @@ function startOutboxFlusher(apiUrl) {
32317
32332
  return;
32318
32333
  try {
32319
32334
  while (!stopped) {
32320
- const r = await flushOutboxOnce(apiUrl);
32335
+ const r = await flushOutboxOnce(apiUrl, wsId);
32321
32336
  if (r.attempted < BATCH_SIZE)
32322
32337
  break;
32323
32338
  }
@@ -32339,7 +32354,7 @@ import { join as join9, dirname as dirname8 } from "path";
32339
32354
  // package.json
32340
32355
  var package_default2 = {
32341
32356
  name: "@shipers-dev/multi",
32342
- version: "0.25.0",
32357
+ version: "0.32.0",
32343
32358
  type: "module",
32344
32359
  bin: {
32345
32360
  "multi-agent": "./dist/index.js"
@@ -32388,20 +32403,31 @@ function log3(msg) {
32388
32403
  appendFileSync4(LOG_PATH3, line);
32389
32404
  process.stdout.write(line);
32390
32405
  }
32391
- async function patchIssueStatus(apiUrl, issueId, status) {
32392
- return apiClient.post(`${apiUrl}/api/issues/agent/mutate`, { action: "update", id: issueId, status });
32406
+ function nestedIssueBase(apiUrl, wsId, projectId, issueId) {
32407
+ return `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
32408
+ }
32409
+ async function patchIssueStatus(apiUrl, wsId, issueId, status) {
32410
+ if (!wsId)
32411
+ return { success: false, error: "no workspace id" };
32412
+ return apiClient.post(`${apiUrl}/api/workspaces/${wsId}/agent/issues/mutate`, { action: "update", id: issueId, status });
32393
32413
  }
32394
- async function markStopped(apiUrl, issueId, reason) {
32414
+ async function markStopped(apiUrl, task, reason) {
32415
+ const wsId = task?.tenant_workspace_id ?? null;
32416
+ const pid = task?.project_id ?? null;
32417
+ const issueId = task?.issue_id;
32395
32418
  try {
32396
- await patchIssueStatus(apiUrl, issueId, "stopped");
32419
+ if (wsId)
32420
+ await patchIssueStatus(apiUrl, wsId, issueId, "stopped");
32397
32421
  } catch {}
32398
32422
  try {
32399
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/comments`, {
32400
- author_type: "agent",
32401
- author_id: "daemon",
32402
- author_name: "daemon",
32403
- body: `⏹ Stopped: ${reason}`
32404
- });
32423
+ if (wsId && pid) {
32424
+ await apiClient.post(`${nestedIssueBase(apiUrl, wsId, pid, issueId)}/comments`, {
32425
+ author_type: "agent",
32426
+ author_id: "daemon",
32427
+ author_name: "daemon",
32428
+ body: `⏹ Stopped: ${reason}`
32429
+ });
32430
+ }
32405
32431
  } catch {}
32406
32432
  await postStream(apiUrl, issueId, "stopped", { reason });
32407
32433
  }
@@ -32428,6 +32454,9 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
32428
32454
  const issueId = task.issue_id;
32429
32455
  const isFollowup = !!task.followup;
32430
32456
  const baseWorkingDir = task.working_dir && existsSync9(task.working_dir) ? task.working_dir : undefined;
32457
+ const tenantWsId = task.tenant_workspace_id ?? null;
32458
+ const projectId = task.project_id ?? null;
32459
+ const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
32431
32460
  let workingDir = baseWorkingDir;
32432
32461
  let worktreeBranch = "";
32433
32462
  if (baseWorkingDir) {
@@ -32436,9 +32465,9 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
32436
32465
  workingDir = wt.path;
32437
32466
  worktreeBranch = wt.branch;
32438
32467
  await postStream(apiUrl, issueId, "worktree_created", { path: wt.path, branch: wt.branch, reused: !wt.created });
32439
- if (task.workspace_id && task.project_id) {
32468
+ if (task.workspace_id && task.project_id && task.tenant_workspace_id) {
32440
32469
  try {
32441
- await apiClient.post(`${apiUrl}/api/agent-workspaces/${task.workspace_id}/ready?project_id=${encodeURIComponent(task.project_id)}`, { status: "ready", worktree_path: wt.path });
32470
+ await apiClient.post(`${apiUrl}/api/workspaces/${task.tenant_workspace_id}/agent-workspaces/${task.workspace_id}/ready?project_id=${encodeURIComponent(task.project_id)}`, { status: "ready", worktree_path: wt.path });
32442
32471
  } catch (e) {
32443
32472
  await postStream(apiUrl, issueId, "worktree_error", { message: `agent-workspace ack failed: ${fmtError(e)}` });
32444
32473
  }
@@ -32448,13 +32477,14 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
32448
32477
  }
32449
32478
  }
32450
32479
  log3(`▶ run_task ${task.key}: ${isFollowup ? "(follow-up) " : ""}${task.title}${workingDir ? ` [cwd: ${workingDir}${worktreeBranch ? ` @${worktreeBranch}` : ""}]` : ""}`);
32451
- await patchIssueStatus(apiUrl, issueId, "in_progress");
32480
+ if (tenantWsId)
32481
+ await patchIssueStatus(apiUrl, tenantWsId, issueId, "in_progress");
32452
32482
  await postStream(apiUrl, issueId, "progress", { message: `Device ${deviceId} picked up ${isFollowup ? "follow-up" : "task"}` });
32453
32483
  let attachmentRefs = [];
32454
- if (task.from_comment_id) {
32484
+ if (task.from_comment_id && task.tenant_workspace_id && task.project_id) {
32455
32485
  const baseDir = workingDir || join9(MULTI_DIR4, "tmp", issueId);
32456
32486
  const inDir = join9(baseDir, ".multi-in", task.from_comment_id);
32457
- attachmentRefs = await downloadCommentAttachments(apiUrl, task.from_comment_id, inDir);
32487
+ attachmentRefs = await downloadCommentAttachments(apiUrl, task.tenant_workspace_id, task.project_id, issueId, task.from_comment_id, inDir);
32458
32488
  if (attachmentRefs.length)
32459
32489
  log3(` fetched ${attachmentRefs.length} attachment(s) → ${inDir}`);
32460
32490
  }
@@ -32467,8 +32497,10 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
32467
32497
  const ensureLiveComment = () => {
32468
32498
  if (liveCommentId)
32469
32499
  return Promise.resolve();
32500
+ if (!ISSUE_BASE)
32501
+ return Promise.resolve();
32470
32502
  if (!liveCommentPromise) {
32471
- liveCommentPromise = apiClient.post(`${apiUrl}/api/issues/${issueId}/comments`, {
32503
+ liveCommentPromise = apiClient.post(`${ISSUE_BASE}/comments`, {
32472
32504
  author_type: "agent",
32473
32505
  author_id: task.agent_id,
32474
32506
  author_name: "agent",
@@ -32480,15 +32512,17 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
32480
32512
  return liveCommentPromise;
32481
32513
  };
32482
32514
  const patchLive = async (body) => {
32483
- if (!liveCommentId)
32515
+ if (!liveCommentId || !ISSUE_BASE)
32484
32516
  return;
32485
32517
  try {
32486
- await apiClient.patch(`${apiUrl}/api/issues/${issueId}/comments/${liveCommentId}`, { body: body || "…" });
32518
+ await apiClient.patch(`${ISSUE_BASE}/comments/${liveCommentId}`, { body: body || "…" });
32487
32519
  } catch {}
32488
32520
  };
32489
32521
  const postComment = async (body) => {
32522
+ if (!ISSUE_BASE)
32523
+ return;
32490
32524
  try {
32491
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body });
32525
+ await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body });
32492
32526
  } catch {}
32493
32527
  };
32494
32528
  const turn = {
@@ -32680,9 +32714,11 @@ _${bits.join(" · ")}_`);
32680
32714
  let preferType = task.agent_runtime || undefined;
32681
32715
  if (!preferType) {
32682
32716
  try {
32683
- const a = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}`);
32684
- if (a.data?.type)
32685
- preferType = a.data.type;
32717
+ if (tenantWsId) {
32718
+ const a = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsId}/agents/${task.agent_id}`);
32719
+ if (a.data?.type)
32720
+ preferType = a.data.type;
32721
+ }
32686
32722
  } catch {}
32687
32723
  }
32688
32724
  const acpCapable = detected.filter((d) => d.type === "claude-code");
@@ -32724,35 +32760,36 @@ Respond in the chat. Only if you produce large artifact files (screenshots, data
32724
32760
  }
32725
32761
  let preamble = "";
32726
32762
  try {
32727
- const agentRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}`);
32728
- const agent = agentRes.data;
32729
- if (agent?.prompt)
32730
- preamble += `# Agent instructions
32763
+ if (tenantWsId) {
32764
+ const agentRes = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsId}/agents/${task.agent_id}`);
32765
+ const agent = agentRes.data;
32766
+ if (agent?.prompt)
32767
+ preamble += `# Agent instructions
32731
32768
 
32732
32769
  ${agent.prompt}
32733
32770
 
32734
32771
  `;
32735
- const skillsRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}/skills`);
32736
- const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
32737
- if (skillList.length) {
32738
- preamble += `# Attached skills (${skillList.length})
32772
+ const skillsRes = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsId}/agents/${task.agent_id}/skills`);
32773
+ const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
32774
+ if (skillList.length) {
32775
+ preamble += `# Attached skills (${skillList.length})
32739
32776
 
32740
32777
  `;
32741
- for (const s of skillList) {
32742
- const body = String(s.body || s.description || "").trim();
32743
- if (!body)
32744
- continue;
32745
- preamble += `## ${s.name}${s.version ? ` v${s.version}` : ""}
32778
+ for (const s of skillList) {
32779
+ const body = String(s.body || s.description || "").trim();
32780
+ if (!body)
32781
+ continue;
32782
+ preamble += `## ${s.name}${s.version ? ` v${s.version}` : ""}
32746
32783
 
32747
32784
  ${body}
32748
32785
 
32749
32786
  `;
32787
+ }
32750
32788
  }
32751
32789
  }
32752
32790
  } catch (e) {
32753
32791
  log3(`preamble fetch failed: ${String(e)}`);
32754
32792
  }
32755
- preamble += await fetchMemoryPreamble(apiUrl, task);
32756
32793
  preamble += await buildPlanningPreamble(apiUrl, task);
32757
32794
  const prompt = preamble ? `${preamble}
32758
32795
  ---
@@ -32770,6 +32807,7 @@ ${userPart}` : userPart;
32770
32807
  issueId,
32771
32808
  deviceId,
32772
32809
  prompt,
32810
+ tenantWsId,
32773
32811
  workspaceId: task.workspace_id || issueId,
32774
32812
  workspaceSessionId: task.workspace_session_id || null,
32775
32813
  sessionId: task.session_id || null,
@@ -32779,11 +32817,14 @@ ${userPart}` : userPart;
32779
32817
  onEvent: eventHandler,
32780
32818
  onSession: async (sid) => {
32781
32819
  try {
32782
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/session`, { session_id: sid });
32820
+ if (ISSUE_BASE)
32821
+ await apiClient.post(`${ISSUE_BASE}/session`, { session_id: sid });
32783
32822
  } catch {}
32784
32823
  if (task.workspace_session_id) {
32785
32824
  try {
32786
- await apiClient.patch(`${apiUrl}/api/sessions/${task.workspace_session_id}?workspace_id=${encodeURIComponent(task.workspace_id || issueId)}`, { provider_session_id: sid, status: "running" });
32825
+ if (tenantWsId) {
32826
+ await apiClient.patch(`${apiUrl}/api/workspaces/${tenantWsId}/sessions/${task.workspace_session_id}?session_workspace_id=${encodeURIComponent(task.workspace_id || issueId)}`, { provider_session_id: sid, status: "running" });
32827
+ }
32787
32828
  } catch {}
32788
32829
  }
32789
32830
  },
@@ -32800,32 +32841,33 @@ ${userPart}` : userPart;
32800
32841
  } else if (useAcpx) {
32801
32842
  let preamble = "";
32802
32843
  try {
32803
- const agentRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}`);
32804
- const agent = agentRes.data;
32805
- if (agent?.prompt)
32806
- preamble += `# Agent instructions
32844
+ if (tenantWsId) {
32845
+ const agentRes = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsId}/agents/${task.agent_id}`);
32846
+ const agent = agentRes.data;
32847
+ if (agent?.prompt)
32848
+ preamble += `# Agent instructions
32807
32849
 
32808
32850
  ${agent.prompt}
32809
32851
 
32810
32852
  `;
32811
- const skillsRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}/skills`);
32812
- const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
32813
- if (skillList.length) {
32814
- preamble += `# Attached skills (${skillList.length})
32853
+ const skillsRes = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsId}/agents/${task.agent_id}/skills`);
32854
+ const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
32855
+ if (skillList.length) {
32856
+ preamble += `# Attached skills (${skillList.length})
32815
32857
 
32816
32858
  `;
32817
- for (const s of skillList) {
32818
- const body = String(s.body || s.description || "").trim();
32819
- if (body)
32820
- preamble += `## ${s.name}${s.version ? ` v${s.version}` : ""}
32859
+ for (const s of skillList) {
32860
+ const body = String(s.body || s.description || "").trim();
32861
+ if (body)
32862
+ preamble += `## ${s.name}${s.version ? ` v${s.version}` : ""}
32821
32863
 
32822
32864
  ${body}
32823
32865
 
32824
32866
  `;
32867
+ }
32825
32868
  }
32826
32869
  }
32827
32870
  } catch {}
32828
- preamble += await fetchMemoryPreamble(apiUrl, task);
32829
32871
  preamble += await buildPlanningPreamble(apiUrl, task);
32830
32872
  const issueContext = `## Issue ${task.key}: ${task.title}${task.description ? `
32831
32873
 
@@ -32885,8 +32927,8 @@ ${userPart}` : userPart;
32885
32927
  for await (const event of runner(task))
32886
32928
  await eventHandler(event);
32887
32929
  }
32888
- if (liveCommentId) {
32889
- const n = await uploadOutputDir(apiUrl, liveCommentId, outDir, task.issue_id);
32930
+ if (liveCommentId && task.tenant_workspace_id && task.project_id) {
32931
+ const n = await uploadOutputDir(apiUrl, task.tenant_workspace_id, task.project_id, task.issue_id, liveCommentId, outDir);
32890
32932
  if (n > 0)
32891
32933
  log3(` \uD83D\uDCCE uploaded ${n} output file(s)`);
32892
32934
  }
@@ -32898,7 +32940,8 @@ ${userPart}` : userPart;
32898
32940
  const summary5 = await executePlanActions(apiUrl, task, actions, ctx);
32899
32941
  if (summary5) {
32900
32942
  try {
32901
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body: summary5 });
32943
+ if (ISSUE_BASE)
32944
+ await apiClient.post(`${ISSUE_BASE}/comments`, { author_type: "agent", author_id: task.agent_id, author_name: "agent", body: summary5 });
32902
32945
  } catch {}
32903
32946
  }
32904
32947
  }
@@ -32909,15 +32952,17 @@ ${userPart}` : userPart;
32909
32952
  log3(` ⚠ ${task.key} produced no assistant output (stopReason=${stopReason})`);
32910
32953
  }
32911
32954
  if (ctx?.runEntry?.stopped) {
32912
- await markStopped(apiUrl, issueId, ctx.runEntry.stopReason || "stopped");
32955
+ await markStopped(apiUrl, task, ctx.runEntry.stopReason || "stopped");
32913
32956
  log3(` ⏹ ${task.key} stopped`);
32914
32957
  } else if (hadError) {
32915
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {});
32958
+ if (ISSUE_BASE)
32959
+ await apiClient.post(`${ISSUE_BASE}/fail`, {});
32916
32960
  log3(` ✗ ${task.key} failed`);
32917
32961
  if (baseWorkingDir)
32918
32962
  await gcWorktreeAfterTerminal(baseWorkingDir, task.key || issueId);
32919
32963
  } else {
32920
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/complete`, {});
32964
+ if (ISSUE_BASE)
32965
+ await apiClient.post(`${ISSUE_BASE}/complete`, {});
32921
32966
  log3(` ✓ ${task.key} complete`);
32922
32967
  if (baseWorkingDir)
32923
32968
  await gcWorktreeAfterTerminal(baseWorkingDir, task.key || issueId);
@@ -32925,64 +32970,18 @@ ${userPart}` : userPart;
32925
32970
  } catch (e) {
32926
32971
  const msg = fmtError(e);
32927
32972
  if (ctx?.runEntry?.stopped) {
32928
- await markStopped(apiUrl, issueId, ctx.runEntry.stopReason || "stopped");
32973
+ await markStopped(apiUrl, task, ctx.runEntry.stopReason || "stopped");
32929
32974
  log3(` ⏹ ${task.key} stopped (${msg})`);
32930
32975
  } else {
32931
32976
  await postStream(apiUrl, issueId, "error", { message: msg });
32932
32977
  await postComment(`❌ spawn error: ${msg}`);
32933
- await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {});
32978
+ if (ISSUE_BASE)
32979
+ await apiClient.post(`${ISSUE_BASE}/fail`, {});
32934
32980
  log3(` ✗ ${task.key} failed: ${msg}`);
32935
32981
  }
32936
32982
  }
32937
32983
  }
32938
- async function fetchMemoryPreamble(apiUrl, task) {
32939
- if (process.env.MULTI_MEMORY_ENABLED !== "1")
32940
- return "";
32941
- try {
32942
- const issueRes = await apiClient.get(`${apiUrl}/api/issues/${task.issue_id}`);
32943
- const projectId = issueRes.data?.project_id;
32944
- if (!projectId)
32945
- return "";
32946
- const title = String(task.title || "").trim();
32947
- const desc = String(task.description || "").trim();
32948
- const followup = String(task.followup || "").trim();
32949
- const query = [title, desc, followup].filter(Boolean).join(`
32950
- `).slice(0, 4000);
32951
- if (!query)
32952
- return "";
32953
- const res = await apiClient.post(`${apiUrl}/api/memory/recall`, {
32954
- project_id: projectId,
32955
- issue_id: task.issue_id,
32956
- query,
32957
- k: 10
32958
- });
32959
- if (!res.success)
32960
- return "";
32961
- const synthesis = String(res.data?.synthesis || "").trim();
32962
- if (!synthesis)
32963
- return "";
32964
- const cites = Array.isArray(res.data?.citations) ? res.data.citations : [];
32965
- let block = `## Project memory
32966
-
32967
- ${synthesis}
32968
- `;
32969
- if (cites.length) {
32970
- block += `
32971
- `;
32972
- for (let i = 0;i < cites.length; i++) {
32973
- const c = cites[i];
32974
- block += `[${i + 1}] ${String(c.snippet || "").replace(/\s+/g, " ").trim()}
32975
- `;
32976
- }
32977
- }
32978
- return `${block}
32979
- `;
32980
- } catch (e) {
32981
- log3(`memory recall failed: ${String(e?.message || e)}`);
32982
- return "";
32983
- }
32984
- }
32985
- async function buildPlanningPreamble(apiUrl, task) {
32984
+ async function buildPlanningPreamble(apiUrl, task, _wsId) {
32986
32985
  const depth = typeof task.planning_depth === "number" ? task.planning_depth : 0;
32987
32986
  if (depth >= PLANNING_DEPTH_LIMIT) {
32988
32987
  return `# Planning (sub-task context)
@@ -32995,20 +32994,25 @@ You are acting on a sub-issue spawned by another agent. You MAY emit a \`multi-p
32995
32994
 
32996
32995
  `;
32997
32996
  }
32998
- let projectId = "";
32997
+ let projectId = task.project_id || "";
32999
32998
  let agentsBlock = "";
33000
32999
  try {
33001
- const issueRes = await apiClient.get(`${apiUrl}/api/issues/${task.issue_id}`);
33002
- projectId = issueRes.data?.project_id || "";
33003
- const agentRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}`);
33004
- const workspaceId = agentRes.data?.workspace_id;
33005
- if (workspaceId) {
33006
- const ag = await apiClient.get(`${apiUrl}/api/agents?workspace_id=${workspaceId}`);
33007
- const list = Array.isArray(ag.data) ? ag.data : [];
33008
- const others = list.filter((a) => a.id !== task.agent_id);
33009
- if (others.length) {
33010
- agentsBlock = others.map((a) => `- \`${a.id}\` (${a.name}${a.type ? `, ${a.type}` : ""})`).join(`
33000
+ if (!projectId && task.tenant_workspace_id) {
33001
+ const issueRes = await apiClient.get(`${apiUrl}/api/workspaces/${task.tenant_workspace_id}/issues/${task.issue_id}`);
33002
+ projectId = issueRes.data?.project_id || "";
33003
+ }
33004
+ const tenantWsIdLocal = task.tenant_workspace_id;
33005
+ if (tenantWsIdLocal) {
33006
+ const agentRes = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsIdLocal}/agents/${task.agent_id}`);
33007
+ const workspaceId = agentRes.data?.workspace_id;
33008
+ if (workspaceId) {
33009
+ const ag = await apiClient.get(`${apiUrl}/api/workspaces/${workspaceId}/agents`);
33010
+ const list = Array.isArray(ag.data) ? ag.data : [];
33011
+ const others = list.filter((a) => a.id !== task.agent_id);
33012
+ if (others.length) {
33013
+ agentsBlock = others.map((a) => `- \`${a.id}\` (${a.name}${a.type ? `, ${a.type}` : ""})`).join(`
33011
33014
  `);
33015
+ }
33012
33016
  }
33013
33017
  }
33014
33018
  } catch {}
@@ -33022,10 +33026,18 @@ Issue actions:
33022
33026
  {"actions":[
33023
33027
  {"type":"create","title":"...","description":"... (required, non-empty: relays full task context to the assignee)","assignee_type":"agent","assignee_id":"<agent id>"},
33024
33028
  {"type":"update","id":"<issue id>","status":"done"},
33025
- {"type":"delegate","id":"<issue id>","assignee_id":"<agent id>"}
33029
+ {"type":"delegate","id":"<issue id>","assignee_id":"<agent id>"},
33030
+ {"type":"issue.delete","id":"<issue id or key>"},
33031
+ {"type":"issue.delete_where","status":"todo"},
33032
+ {"type":"issue.list","status":"todo","assignee_id":"<agent id>","limit":20},
33033
+ {"type":"issue.search","query":"flaky tests","limit":10}
33026
33034
  ]}
33027
33035
  \`\`\`
33028
33036
 
33037
+ 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.
33038
+
33039
+ 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).
33040
+
33029
33041
  Agent + skill self-service (use sparingly — only when you genuinely need a new capability that isn't covered by an existing agent or skill):
33030
33042
 
33031
33043
  \`\`\`multi-plan
@@ -33045,7 +33057,9 @@ Rules:
33045
33057
  - \`agent.create\` is auto-approved on auto-autonomy issues. Caps + rate limits prevent abuse.
33046
33058
  - \`skill.create\` ALWAYS waits for human review (skill bodies become future system prompts).
33047
33059
  - \`allowed_tools\` on a new agent must be a subset of your own tools.
33048
- - \`create\` / \`update\` / \`delegate\` target issues only in the current project (${projectId || "this project"}).
33060
+ - \`create\` / \`update\` / \`delegate\` / \`issue.delete\` target issues only in the current project (${projectId || "this project"}).
33061
+ - \`issue.list\` / \`issue.search\` / \`issue.delete_where\` default to the current project; pass \`project_id\` only when crossing into another project in the same workspace.
33062
+ - \`issue.delete_where\` requires \`status\` or \`assignee_id\` (no unfiltered project-wipe). Cap 50 per call.
33049
33063
 
33050
33064
  ${agentsBlock ? `Available agents you can delegate to:
33051
33065
  ${agentsBlock}
@@ -33082,7 +33096,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx) {
33082
33096
  const blocked3 = actions.filter((a) => a.type !== "update").length;
33083
33097
  actions = actions.filter((a) => a.type === "update");
33084
33098
  if (blocked3)
33085
- lines.push(`- ${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})`);
33099
+ lines.push(`- [warn] ${blocked3} non-update action(s) blocked (planning depth limit ${PLANNING_DEPTH_LIMIT})`);
33086
33100
  }
33087
33101
  const SUBCAPS = { "agent.create": 2, "skill.create": 3, "skill.attach": 5, "skill.detach": 5, "agent.update": 5, "session.create": 3 };
33088
33102
  const counts = {};
@@ -33092,21 +33106,26 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx) {
33092
33106
  return true;
33093
33107
  counts[a.type] = (counts[a.type] || 0) + 1;
33094
33108
  if (counts[a.type] > cap) {
33095
- lines.push(`- ${a.type} sub-cap ${cap} hit, dropping extra`);
33109
+ lines.push(`- [warn] ${a.type} sub-cap ${cap} hit, dropping extra`);
33096
33110
  return false;
33097
33111
  }
33098
33112
  return true;
33099
33113
  });
33100
33114
  const parentId = parentTask.issue_id;
33101
- const parentProjectId = await (async () => {
33115
+ const parentWsId = parentTask.tenant_workspace_id ?? null;
33116
+ const parentProjectId = parentTask.project_id ?? await (async () => {
33117
+ if (!parentWsId)
33118
+ return null;
33102
33119
  try {
33103
- const r = await apiClient.get(`${apiUrl}/api/issues/${parentId}`);
33120
+ const r = await apiClient.get(`${apiUrl}/api/workspaces/${parentWsId}/issues/${parentId}`);
33104
33121
  return r.data?.project_id;
33105
33122
  } catch {
33106
33123
  return null;
33107
33124
  }
33108
33125
  })();
33109
33126
  const headers = { "x-agent-id": parentTask.agent_id, "x-origin-issue-id": parentTask.issue_id };
33127
+ const mutateUrl = parentWsId ? `${apiUrl}/api/workspaces/${parentWsId}/agent/issues/mutate` : "";
33128
+ const queryUrl = parentWsId ? `${apiUrl}/api/workspaces/${parentWsId}/agent/issues/query` : "";
33110
33129
  if (typeof ctx.refreshLocalAgents === "function") {
33111
33130
  try {
33112
33131
  await ctx.refreshLocalAgents();
@@ -33125,83 +33144,172 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx) {
33125
33144
  assignee_id: a.assignee_id,
33126
33145
  parent_id: a.parent_id || parentId
33127
33146
  };
33128
- const res = await apiClient.post(`${apiUrl}/api/issues/agent/mutate`, body, { headers });
33147
+ const res = await apiClient.post(mutateUrl, body, { headers });
33129
33148
  if (!res.success) {
33130
- lines.push(`- create "${a.title}": ${res.error || res.status}`);
33149
+ lines.push(`- [err] create "${a.title}": ${res.error || res.status}`);
33131
33150
  continue;
33132
33151
  }
33133
33152
  const created = res.data;
33134
- lines.push(`- created **${created.key}** ${created.title}${created.assignee_id ? ` @${created.assignee_id}` : ""} (autonomy=${created.autonomy_level || "auto"})`);
33153
+ lines.push(`- [ok] created **${created.key}** - ${created.title}${created.assignee_id ? ` -> @${created.assignee_id}` : ""} (autonomy=${created.autonomy_level || "auto"})`);
33135
33154
  } else if (a.type === "update") {
33136
- const res = await apiClient.post(`${apiUrl}/api/issues/agent/mutate`, { action: "update", ...a }, { headers });
33155
+ const res = await apiClient.post(mutateUrl, { action: "update", ...a }, { headers });
33137
33156
  if (!res.success) {
33138
- lines.push(`- update ${a.id}: ${res.error || res.status}`);
33157
+ lines.push(`- [err] update ${a.id}: ${res.error || res.status}`);
33139
33158
  continue;
33140
33159
  }
33141
- lines.push(`- updated ${res.data.key}`);
33160
+ lines.push(`- [ok] updated ${res.data.key}`);
33142
33161
  if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync9(parentTask.working_dir)) {
33143
33162
  const targetKey = res.data?.key;
33144
33163
  if (targetKey)
33145
33164
  await gcWorktreeAfterTerminal(parentTask.working_dir, targetKey);
33146
33165
  }
33147
33166
  } else if (a.type === "delegate") {
33148
- const res = await apiClient.post(`${apiUrl}/api/issues/agent/mutate`, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "todo" }, { headers });
33167
+ const res = await apiClient.post(mutateUrl, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "todo" }, { headers });
33168
+ if (!res.success) {
33169
+ lines.push(`- [err] delegate ${a.id}: ${res.error || res.status}`);
33170
+ continue;
33171
+ }
33172
+ lines.push(`- [ok] delegated ${res.data.key} -> ${a.assignee_id}`);
33173
+ } else if (a.type === "issue.delete") {
33174
+ const res = await apiClient.post(mutateUrl, { action: "delete", id: a.id }, { headers });
33175
+ if (!res.success) {
33176
+ lines.push(`- [err] issue.delete ${a.id}: ${res.error || res.status}`);
33177
+ continue;
33178
+ }
33179
+ lines.push(`- [ok] deleted ${res.data?.key || a.id}`);
33180
+ } else if (a.type === "issue.delete_where") {
33181
+ const res = await apiClient.post(mutateUrl, {
33182
+ action: "delete_where",
33183
+ project_id: a.project_id || parentProjectId,
33184
+ status: a.status,
33185
+ assignee_id: a.assignee_id,
33186
+ limit: a.limit ?? 50
33187
+ }, { headers });
33149
33188
  if (!res.success) {
33150
- lines.push(`- delegate ${a.id}: ${res.error || res.status}`);
33189
+ lines.push(`- [err] issue.delete_where: ${res.error || res.status}`);
33190
+ continue;
33191
+ }
33192
+ const rows = Array.isArray(res.data?.deleted) ? res.data.deleted : [];
33193
+ const count = res.data?.count ?? rows.length;
33194
+ if (!count) {
33195
+ lines.push(`- [ok] issue.delete_where: 0 matches`);
33196
+ continue;
33197
+ }
33198
+ lines.push(`- [ok] issue.delete_where: deleted ${count} issue(s)`);
33199
+ for (const r of rows)
33200
+ lines.push(` - ${r.key}`);
33201
+ } else if (a.type === "issue.list") {
33202
+ const res = await apiClient.post(queryUrl, {
33203
+ kind: "list",
33204
+ project_id: a.project_id || parentProjectId,
33205
+ status: a.status,
33206
+ assignee_id: a.assignee_id,
33207
+ limit: a.limit ?? 20
33208
+ }, { headers });
33209
+ if (!res.success) {
33210
+ lines.push(`- [err] issue.list: ${res.error || res.status}`);
33211
+ continue;
33212
+ }
33213
+ const rows = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.results) ? res.data.results : [];
33214
+ if (!rows.length) {
33215
+ lines.push(`- [ok] issue.list: 0 issues`);
33216
+ continue;
33217
+ }
33218
+ lines.push(`- [ok] issue.list: ${rows.length} issue(s)`);
33219
+ for (const r of rows) {
33220
+ lines.push(` - **${r.key}** [${r.status}] ${r.title}${r.assignee_id ? ` (@${r.assignee_id})` : ""}`);
33221
+ }
33222
+ } else if (a.type === "issue.search") {
33223
+ const res = await apiClient.post(queryUrl, {
33224
+ kind: "search",
33225
+ project_id: a.project_id || parentProjectId,
33226
+ query: a.query,
33227
+ limit: a.limit ?? 10
33228
+ }, { headers });
33229
+ if (!res.success) {
33230
+ lines.push(`- [err] issue.search "${a.query}": ${res.error || res.status}`);
33231
+ continue;
33232
+ }
33233
+ const rows = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.results) ? res.data.results : [];
33234
+ if (!rows.length) {
33235
+ lines.push(`- [ok] issue.search "${a.query}": 0 hits`);
33151
33236
  continue;
33152
33237
  }
33153
- lines.push(`- delegated ${res.data.key} ${a.assignee_id}`);
33238
+ lines.push(`- [ok] issue.search "${a.query}": ${rows.length} hit(s)`);
33239
+ for (const r of rows) {
33240
+ lines.push(` - **${r.key}** [${r.status}] ${r.title}`);
33241
+ }
33154
33242
  } else if (a.type === "agent.create") {
33155
- const res = await apiClient.post(`${apiUrl}/api/agent_ops/agents/mutate`, { action: "create", name: a.name, type: a.agent_type, prompt: a.prompt, skill_ids: a.skill_ids, allowed_tools: a.allowed_tools }, { headers });
33243
+ if (!parentWsId) {
33244
+ lines.push(`- [err] agent.create "${a.name}": no tenant workspace id`);
33245
+ continue;
33246
+ }
33247
+ const res = await apiClient.post(`${apiUrl}/api/workspaces/${parentWsId}/agent_ops/agents/mutate`, { action: "create", name: a.name, type: a.agent_type, prompt: a.prompt, skill_ids: a.skill_ids, allowed_tools: a.allowed_tools }, { headers });
33156
33248
  if (!res.success) {
33157
- lines.push(`- agent.create "${a.name}": ${res.error || res.status}`);
33249
+ lines.push(`- [err] agent.create "${a.name}": ${res.error || res.status}`);
33158
33250
  continue;
33159
33251
  }
33160
- lines.push(`- agent.create "${a.name}" ${res.data?.agent_id}`);
33252
+ lines.push(`- [ok] agent.create "${a.name}" -> ${res.data?.agent_id}`);
33161
33253
  } else if (a.type === "agent.update") {
33162
- const res = await apiClient.post(`${apiUrl}/api/agent_ops/agents/mutate`, { action: "update", ...a }, { headers });
33254
+ if (!parentWsId) {
33255
+ lines.push(`- [err] agent.update ${a.id}: no tenant workspace id`);
33256
+ continue;
33257
+ }
33258
+ const res = await apiClient.post(`${apiUrl}/api/workspaces/${parentWsId}/agent_ops/agents/mutate`, { action: "update", ...a }, { headers });
33163
33259
  if (!res.success) {
33164
- lines.push(`- agent.update ${a.id}: ${res.error || res.status}`);
33260
+ lines.push(`- [err] agent.update ${a.id}: ${res.error || res.status}`);
33165
33261
  continue;
33166
33262
  }
33167
33263
  if (res.data?.queued)
33168
- lines.push(`- agent.update ${a.id} queued`);
33264
+ lines.push(`- [pending] agent.update ${a.id} queued`);
33169
33265
  else
33170
- lines.push(`- agent.update ${a.id}`);
33266
+ lines.push(`- [ok] agent.update ${a.id}`);
33171
33267
  } else if (a.type === "skill.create") {
33172
- const res = await apiClient.post(`${apiUrl}/api/agent_ops/skills/mutate`, { action: "create", name: a.name, version: a.version, description: a.description, body: a.body, files: a.files }, { headers });
33268
+ if (!parentWsId) {
33269
+ lines.push(`- [err] skill.create "${a.name}": no tenant workspace id`);
33270
+ continue;
33271
+ }
33272
+ const res = await apiClient.post(`${apiUrl}/api/workspaces/${parentWsId}/agent_ops/skills/mutate`, { action: "create", name: a.name, version: a.version, description: a.description, body: a.body, files: a.files }, { headers });
33173
33273
  if (!res.success) {
33174
- lines.push(`- skill.create "${a.name}": ${res.error || res.status}`);
33274
+ lines.push(`- [err] skill.create "${a.name}": ${res.error || res.status}`);
33175
33275
  continue;
33176
33276
  }
33177
- lines.push(`- skill.create "${a.name}" queued for human review (op ${res.data?.pending_op_id})`);
33277
+ lines.push(`- [pending] skill.create "${a.name}" queued for human review (op ${res.data?.pending_op_id})`);
33178
33278
  } else if (a.type === "session.create") {
33179
- const res = await apiClient.post(`${apiUrl}/api/sessions`, { workspace_id: a.workspace_id, agent_id: a.agent_id, role: a.role, device_id: a.device_id }, { headers });
33279
+ if (!parentWsId) {
33280
+ lines.push(`- [err] session.create role=${a.role}: no tenant workspace id`);
33281
+ continue;
33282
+ }
33283
+ const res = await apiClient.post(`${apiUrl}/api/workspaces/${parentWsId}/sessions`, { workspace_id: a.workspace_id, agent_id: a.agent_id, role: a.role, device_id: a.device_id }, { headers });
33180
33284
  if (!res.success) {
33181
- lines.push(`- session.create role=${a.role}: ${res.error || res.status}`);
33285
+ lines.push(`- [err] session.create role=${a.role}: ${res.error || res.status}`);
33182
33286
  continue;
33183
33287
  }
33184
33288
  const sess = res.data?.session;
33185
33289
  const reused = res.data?.reused;
33186
- lines.push(`- ${reused ? "" : ""} session.${reused ? "reuse" : "create"} ${sess?.id?.slice(0, 8) || "?"} (role=${a.role})`);
33290
+ lines.push(`- [${reused ? "reuse" : "ok"}] session.${reused ? "reuse" : "create"} ${sess?.id?.slice(0, 8) || "?"} (role=${a.role})`);
33187
33291
  } else if (a.type === "skill.attach" || a.type === "skill.detach") {
33188
33292
  const action = a.type === "skill.attach" ? "attach_skill" : "detach_skill";
33189
- const res = await apiClient.post(`${apiUrl}/api/agent_ops/agents/mutate`, { action, agent_id: a.agent_id, skill_id: a.skill_id }, { headers });
33293
+ if (!parentWsId) {
33294
+ lines.push(`- [err] ${a.type}: no tenant workspace id`);
33295
+ continue;
33296
+ }
33297
+ const res = await apiClient.post(`${apiUrl}/api/workspaces/${parentWsId}/agent_ops/agents/mutate`, { action, agent_id: a.agent_id, skill_id: a.skill_id }, { headers });
33190
33298
  if (!res.success) {
33191
- lines.push(`- ${a.type} ${a.skill_id}→${a.agent_id}: ${res.error || res.status}`);
33299
+ lines.push(`- [err] ${a.type} ${a.skill_id}->${a.agent_id}: ${res.error || res.status}`);
33192
33300
  continue;
33193
33301
  }
33194
33302
  if (res.data?.queued)
33195
- lines.push(`- ${a.type} queued`);
33303
+ lines.push(`- [pending] ${a.type} queued`);
33196
33304
  else
33197
- lines.push(`- ${a.type} ${a.skill_id} ${a.agent_id}`);
33305
+ lines.push(`- [ok] ${a.type} ${a.skill_id} <-> ${a.agent_id}`);
33198
33306
  }
33199
33307
  } catch (e) {
33200
- lines.push(`- ${a.type} failed: ${String(e)}`);
33308
+ lines.push(`- [err] ${a.type} failed: ${String(e)}`);
33201
33309
  }
33202
33310
  }
33203
33311
  if (truncated)
33204
- lines.push(`- action list truncated at ${PLAN_ACTION_LIMIT}`);
33312
+ lines.push(`- [warn] action list truncated at ${PLAN_ACTION_LIMIT}`);
33205
33313
  if (!lines.length)
33206
33314
  return "";
33207
33315
  return `**Planning actions**
@@ -33289,9 +33397,10 @@ async function postStream(_apiUrl, issueId, event_type, payload) {
33289
33397
  } catch {}
33290
33398
  }
33291
33399
  }
33292
- async function downloadCommentAttachments(apiUrl, commentId, destDir) {
33400
+ async function downloadCommentAttachments(apiUrl, wsId, projectId, issueId, commentId, destDir) {
33401
+ const issueBase = `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
33293
33402
  try {
33294
- const list = await apiClient.get(`${apiUrl}/api/attachments/comments/${commentId}`);
33403
+ const list = await apiClient.get(`${issueBase}/comments/${commentId}/attachments`);
33295
33404
  const items = list.data?.results || list.data || [];
33296
33405
  if (!Array.isArray(items) || items.length === 0)
33297
33406
  return [];
@@ -33299,7 +33408,7 @@ async function downloadCommentAttachments(apiUrl, commentId, destDir) {
33299
33408
  const token = authTokenHeader();
33300
33409
  const out = [];
33301
33410
  for (const it of items) {
33302
- const res = await fetch(`${apiUrl}/api/attachments/${it.id}`, { headers: token ? { Authorization: token } : {} });
33411
+ const res = await fetch(`${issueBase}/attachments/${it.id}`, { headers: token ? { Authorization: token } : {} });
33303
33412
  if (!res.ok)
33304
33413
  continue;
33305
33414
  const buf = new Uint8Array(await res.arrayBuffer());
@@ -33317,7 +33426,7 @@ function authTokenHeader() {
33317
33426
  const cfg = loadConfig();
33318
33427
  return cfg.token ? `Bearer ${cfg.token}` : null;
33319
33428
  }
33320
- async function uploadOutputDir(apiUrl, commentId, dir, issueId) {
33429
+ async function uploadOutputDir(apiUrl, wsId, projectId, issueId, commentId, dir) {
33321
33430
  if (!existsSync9(dir))
33322
33431
  return 0;
33323
33432
  const files = [];
@@ -33339,6 +33448,7 @@ async function uploadOutputDir(apiUrl, commentId, dir, issueId) {
33339
33448
  if (!files.length)
33340
33449
  return 0;
33341
33450
  const token = authTokenHeader();
33451
+ const issueBase = `${apiUrl}/api/workspaces/${wsId}/projects/${projectId}/issues/${issueId}`;
33342
33452
  let uploaded = 0;
33343
33453
  for (const f of files) {
33344
33454
  try {
@@ -33349,9 +33459,7 @@ async function uploadOutputDir(apiUrl, commentId, dir, issueId) {
33349
33459
  const headers = {};
33350
33460
  if (token)
33351
33461
  headers.Authorization = token;
33352
- if (issueId)
33353
- headers["x-issue-id"] = issueId;
33354
- const res = await fetch(`${apiUrl}/api/attachments/comments/${commentId}`, {
33462
+ const res = await fetch(`${issueBase}/comments/${commentId}/attachments`, {
33355
33463
  method: "POST",
33356
33464
  body: form,
33357
33465
  headers
@@ -33606,6 +33714,14 @@ function sleep6(ms) {
33606
33714
  function openTasksDb() {
33607
33715
  ensureDirs2();
33608
33716
  const db2 = new Database3(TASKS_DB_PATH4);
33717
+ const existing = db2.query("PRAGMA table_info(tasks)").all();
33718
+ if (existing.length) {
33719
+ const have = new Set(existing.map((c) => c.name));
33720
+ const wanted = ["id", "status", "payload", "attempts", "created_at", "started_at", "finished_at", "error", "agent_id", "issue_id"];
33721
+ const compatible = wanted.every((col) => have.has(col));
33722
+ if (!compatible)
33723
+ db2.exec("DROP TABLE tasks");
33724
+ }
33609
33725
  db2.exec(`
33610
33726
  CREATE TABLE IF NOT EXISTS tasks (
33611
33727
  id TEXT PRIMARY KEY,
@@ -33615,23 +33731,15 @@ function openTasksDb() {
33615
33731
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
33616
33732
  started_at INTEGER,
33617
33733
  finished_at INTEGER,
33618
- error TEXT
33734
+ error TEXT,
33735
+ agent_id TEXT,
33736
+ issue_id TEXT
33619
33737
  );
33620
33738
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
33621
- `);
33622
- const cols = db2.query("PRAGMA table_info(tasks)").all();
33623
- const have = new Set(cols.map((c) => c.name));
33624
- if (!have.has("agent_id"))
33625
- db2.exec("ALTER TABLE tasks ADD COLUMN agent_id TEXT");
33626
- if (!have.has("issue_id"))
33627
- db2.exec("ALTER TABLE tasks ADD COLUMN issue_id TEXT");
33628
- db2.exec(`
33629
33739
  CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id, status);
33630
33740
  CREATE INDEX IF NOT EXISTS idx_tasks_issue ON tasks(issue_id);
33631
33741
  `);
33632
33742
  db2.run("UPDATE tasks SET status = 'queued' WHERE status = 'pending'");
33633
- db2.run("UPDATE tasks SET agent_id = json_extract(payload, '$.agent_id') WHERE agent_id IS NULL AND payload IS NOT NULL AND json_valid(payload)");
33634
- db2.run("UPDATE tasks SET issue_id = json_extract(payload, '$.issue_id') WHERE issue_id IS NULL AND payload IS NOT NULL AND json_valid(payload)");
33635
33743
  return db2;
33636
33744
  }
33637
33745
  async function pickFreePort() {
@@ -33753,16 +33861,20 @@ async function probeTunnel(url2) {
33753
33861
  return false;
33754
33862
  }
33755
33863
  }
33756
- async function announceTunnel(apiUrl, deviceId, tunnelUrl) {
33864
+ async function announceTunnel(apiUrl, wsId, deviceId, tunnelUrl) {
33865
+ if (!wsId) {
33866
+ log3(`announce-tunnel skipped: no workspace id`);
33867
+ return;
33868
+ }
33757
33869
  try {
33758
- await apiClient.post(`${apiUrl}/api/devices/${deviceId}/announce-tunnel`, { tunnel_url: tunnelUrl });
33870
+ await apiClient.post(`${apiUrl}/api/workspaces/${wsId}/devices/${deviceId}/announce-tunnel`, { tunnel_url: tunnelUrl });
33759
33871
  } catch (e) {
33760
33872
  log3(`announce-tunnel failed: ${String(e?.message ?? e)}`);
33761
33873
  }
33762
33874
  }
33763
- async function ackDispatch(apiUrl, dispatchId, secret) {
33875
+ async function ackDispatch(apiUrl, wsId, dispatchId, secret) {
33764
33876
  try {
33765
- await fetch(`${apiUrl}/api/issues/dispatches/${dispatchId}/ack`, {
33877
+ await fetch(`${apiUrl}/api/workspaces/${wsId}/dispatches/${dispatchId}/ack`, {
33766
33878
  method: "POST",
33767
33879
  headers: { authorization: `Bearer ${secret}` }
33768
33880
  });
@@ -33770,12 +33882,12 @@ async function ackDispatch(apiUrl, dispatchId, secret) {
33770
33882
  log3(`ack dispatch ${dispatchId} failed: ${String(e)}`);
33771
33883
  }
33772
33884
  }
33773
- async function drainOfflineDispatches(apiUrl, deviceId, secret, db2, onEnqueued, isAlive) {
33885
+ async function drainOfflineDispatches(apiUrl, deviceId, wsId, secret, db2, onEnqueued, isAlive) {
33774
33886
  if (!isAlive())
33775
33887
  return;
33776
33888
  let list;
33777
33889
  try {
33778
- list = await apiClient.get(`${apiUrl}/api/devices/${deviceId}/dispatches/pending`);
33890
+ list = await apiClient.get(`${apiUrl}/api/workspaces/${wsId}/devices/${deviceId}/dispatches/pending`);
33779
33891
  } catch (e) {
33780
33892
  log3(`drain list failed: ${String(e)}`);
33781
33893
  return;
@@ -33790,7 +33902,7 @@ async function drainOfflineDispatches(apiUrl, deviceId, secret, db2, onEnqueued,
33790
33902
  return;
33791
33903
  }
33792
33904
  try {
33793
- const res = await fetch(`${apiUrl}/api/issues/dispatches/${r.id}/claim`, {
33905
+ const res = await fetch(`${apiUrl}/api/workspaces/${wsId}/dispatches/${r.id}/claim`, {
33794
33906
  method: "POST",
33795
33907
  headers: { authorization: `Bearer ${secret}` }
33796
33908
  });
@@ -33803,7 +33915,7 @@ async function drainOfflineDispatches(apiUrl, deviceId, secret, db2, onEnqueued,
33803
33915
  db2.run("INSERT INTO tasks (id, status, payload, agent_id, issue_id) VALUES (?, 'queued', ?, ?, ?)", [taskId, JSON.stringify(task), task.agent_id ?? null, task.issue_id ?? null]);
33804
33916
  if (task.issue_id)
33805
33917
  postStream(apiUrl, task.issue_id, "queued", { drained: true });
33806
- ackDispatch(apiUrl, r.id, secret);
33918
+ ackDispatch(apiUrl, wsId, r.id, secret);
33807
33919
  } catch (e) {
33808
33920
  log3(`drain ${r.id} failed: ${String(e)}`);
33809
33921
  }
@@ -33901,8 +34013,8 @@ async function daemonMain({ cfg, apiUrl }) {
33901
34013
  const pos = db2.query("SELECT COUNT(*) AS c FROM tasks WHERE status = 'queued' AND created_at <= (SELECT created_at FROM tasks WHERE id = ?)").get(taskId)?.c ?? 1;
33902
34014
  if (t.issue_id)
33903
34015
  postStream(apiUrl, t.issue_id, "queued", { queue_position: pos });
33904
- if (t.dispatch_id)
33905
- ackDispatch(apiUrl, t.dispatch_id, cfg.dispatchSecret);
34016
+ if (t.dispatch_id && cfg.workspaceId)
34017
+ ackDispatch(apiUrl, cfg.workspaceId, t.dispatch_id, cfg.dispatchSecret);
33906
34018
  queueMicrotask(() => schedule2());
33907
34019
  return Response.json({ accepted: true, task_id: taskId }, { status: 202 });
33908
34020
  } catch (e) {
@@ -33967,11 +34079,13 @@ async function daemonMain({ cfg, apiUrl }) {
33967
34079
  log3(`☁️ Tunnel up: ${tunnel.url}`);
33968
34080
  }
33969
34081
  let alive = true;
33970
- await announceTunnel(apiUrl, cfg.deviceId, tunnel?.url ?? null);
34082
+ await announceTunnel(apiUrl, cfg.workspaceId, cfg.deviceId, tunnel?.url ?? null);
33971
34083
  try {
33972
- await materializeBundle(apiUrl, cfg.deviceId, log3);
34084
+ await materializeBundle(apiUrl, cfg.workspaceId, cfg.deviceId, log3);
33973
34085
  } catch {}
33974
- drainOfflineDispatches(apiUrl, cfg.deviceId, cfg.dispatchSecret, db2, () => schedule2(), () => alive);
34086
+ if (cfg.workspaceId) {
34087
+ drainOfflineDispatches(apiUrl, cfg.deviceId, cfg.workspaceId, cfg.dispatchSecret, db2, () => schedule2(), () => alive);
34088
+ }
33975
34089
  const shutdown = async (reason) => {
33976
34090
  if (!alive)
33977
34091
  return;
@@ -33984,7 +34098,7 @@ async function daemonMain({ cfg, apiUrl }) {
33984
34098
  tunnel?.child?.kill();
33985
34099
  } catch {}
33986
34100
  try {
33987
- await announceTunnel(apiUrl, cfg.deviceId, null);
34101
+ await announceTunnel(apiUrl, cfg.workspaceId, cfg.deviceId, null);
33988
34102
  } catch {}
33989
34103
  if (existsSync10(PID_PATH3))
33990
34104
  unlinkSync5(PID_PATH3);
@@ -34016,8 +34130,10 @@ async function daemonMain({ cfg, apiUrl }) {
34016
34130
  if (next) {
34017
34131
  tunnel = next;
34018
34132
  log3(`☁️ Tunnel up: ${tunnel.url}`);
34019
- await announceTunnel(apiUrl, cfg.deviceId, tunnel.url);
34020
- drainOfflineDispatches(apiUrl, cfg.deviceId, cfg.dispatchSecret, db2, () => schedule2(), () => alive);
34133
+ await announceTunnel(apiUrl, cfg.workspaceId, cfg.deviceId, tunnel.url);
34134
+ if (cfg.workspaceId) {
34135
+ drainOfflineDispatches(apiUrl, cfg.deviceId, cfg.workspaceId, cfg.dispatchSecret, db2, () => schedule2(), () => alive);
34136
+ }
34021
34137
  return;
34022
34138
  }
34023
34139
  const wait = Math.min(30000, 2000 * attempt);
@@ -34168,10 +34284,20 @@ var connectCmd = exports_Effect.fn("connectCmd")(function* () {
34168
34284
  process.on("exit", () => releasePidLock(PID_PATH));
34169
34285
  }
34170
34286
  yield* logger.log(`connect: device=${cfg.deviceId}`);
34171
- const stopFlusher = startOutboxFlusher(cfg.apiUrl);
34287
+ if (!cfg.workspaceId) {
34288
+ return yield* exports_Effect.fail(new UsageError({ message: "config.workspaceId missing — re-pair via 'multi-agent setup'." }));
34289
+ }
34290
+ const stopFlusher = startOutboxFlusher(cfg.apiUrl, cfg.workspaceId);
34172
34291
  yield* exports_Effect.tryPromise({
34173
34292
  try: () => daemonMain({ cfg, apiUrl: cfg.apiUrl }),
34174
- catch: (cause3) => new DaemonError({ message: "daemon failed", cause: cause3 })
34293
+ catch: (cause3) => {
34294
+ try {
34295
+ const err = cause3;
34296
+ console.error(`[daemon] crash: ${err?.message ?? String(err)}
34297
+ ${err?.stack ?? ""}`);
34298
+ } catch {}
34299
+ return new DaemonError({ message: "daemon failed", cause: cause3 });
34300
+ }
34175
34301
  }).pipe(exports_Effect.ensuring(exports_Effect.sync(() => stopFlusher())));
34176
34302
  });
34177
34303
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.25.0",
3
+ "version": "0.32.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"
@@ -21,6 +21,6 @@
21
21
  "undici": "^7.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@multi/lib": "workspace:*"
24
+ "@multi/lib": "0.1.0"
25
25
  }
26
26
  }