@coreplane/switchboard 1.218.0 → 1.219.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.218.0",
3
+ "version": "1.219.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.218.0",
9
+ "version": "1.219.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -16710,7 +16710,7 @@
16710
16710
  },
16711
16711
  "packages/switchboard": {
16712
16712
  "name": "@coreplane/switchboard",
16713
- "version": "1.218.0",
16713
+ "version": "1.219.0",
16714
16714
  "license": "Apache-2.0",
16715
16715
  "dependencies": {
16716
16716
  "@anthropic-ai/sdk": "^0.124.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.218.0",
3
+ "version": "1.219.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.218.0",
3
- "commit": "2931369158bcc02d8e2cfd80eb661bae017188bd",
4
- "builtAt": "2026-09-15T00:34:28.652Z"
2
+ "version": "1.219.0",
3
+ "commit": "584d81012dc5119229ec5e4ea04a304e3eceb450",
4
+ "builtAt": "2026-09-15T01:38:10.165Z"
5
5
  }
@@ -435,6 +435,8 @@ TIME. Your budget is up to two hours — less when a boundary or the request's \
435
435
 
436
436
  READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:coding\`.
437
437
 
438
+ You cannot attach or post files: your whole answer is text. Never say a file is attached or below — name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
439
+
438
440
  Maintain the user-facing status card with the update_status tool: post your plan as a checklist (○ pending) once you have it, and update items as they start (✱) and finish (✓ — only after they actually happened). Items are short outcomes ("Clone and install", "Time the full suite"), never commands.
439
441
 
440
442
  ${NOTEPAD}
@@ -38,7 +38,8 @@ export type FrictionCategory =
38
38
  | "setup_install"
39
39
  | "wrap_up"
40
40
  | "budget_hit"
41
- | "infra_failure";
41
+ | "infra_failure"
42
+ | "unkept_promise";
42
43
 
43
44
  export const FRICTION_CATEGORIES: readonly FrictionCategory[] = [
44
45
  "slow_tool",
@@ -49,6 +50,7 @@ export const FRICTION_CATEGORIES: readonly FrictionCategory[] = [
49
50
  "wrap_up",
50
51
  "budget_hit",
51
52
  "infra_failure",
53
+ "unkept_promise",
52
54
  ];
53
55
 
54
56
  /** Human labels — the verdict line, the report's table and its finding lines. */
@@ -61,6 +63,7 @@ export const CATEGORY_LABEL: Record<FrictionCategory, string> = {
61
63
  wrap_up: "agent wind-down",
62
64
  budget_hit: "budget hits",
63
65
  infra_failure: "infra failures",
66
+ unkept_promise: "unkept promises",
64
67
  };
65
68
 
66
69
  /** What a category's time is a share OF: tool time for the categories whose
@@ -75,8 +78,17 @@ export const DENOMINATOR_OF: Record<FrictionCategory, "tool" | "run"> = {
75
78
  wrap_up: "run",
76
79
  budget_hit: "run",
77
80
  infra_failure: "run",
81
+ unkept_promise: "run",
78
82
  };
79
83
 
84
+ /** The ways a reply tells the person a file is attached — the promise `unkept_promise` holds a run to.
85
+ * One deliberate expression, case-insensitive: "attached below/here/above", "see (the) attached",
86
+ * "I've/I have attached", "is/are attached", "attachment(s) (is/are) below/here". A path such as
87
+ * `./attachments/1-clip.mp4` or the bare word "attachments/" names a place, not a promise, and
88
+ * does not match; nor does prose about attaching in an `assistant` turn — only the `answer` counts. */
89
+ export const ATTACHMENT_PROMISE =
90
+ /\b(?:attached (?:below|here|above)|see (?:the )?attached|i(?:'ve| have) attached|(?:is|are) attached\b|attachments? (?:(?:is|are) )?(?:below|here))/i;
91
+
80
92
  export type FrictionSeverity = "low" | "medium" | "high";
81
93
 
82
94
  export interface FrictionFinding {
@@ -382,6 +394,9 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
382
394
  let sideFactEvents = 0; // skill_use / review_artifact / pr_description / pr_opened / review_posted / ship_round / route: facts about the run, not steps
383
395
  let spanEvents = 0; // span_start / span_end (docs/reference/specs/tracing.md): timing records, not steps
384
396
  let wrapUp: { index: number; at?: number } | undefined;
397
+ // The reply and the files the run sent out: what `unkept_promise` compares after the loop.
398
+ let answer: { index: number; text: string } | undefined;
399
+ let outboundArtifacts = 0;
385
400
  events.forEach((ev, index) => {
386
401
  flushTurns(index);
387
402
  if (isSpanRecord(ev)) {
@@ -390,8 +405,10 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
390
405
  }
391
406
  if (isNarrative(ev)) {
392
407
  narrativeEvents++; // the narrative and `run_meta` are neither steps nor findings
408
+ if (ev.type === "answer") answer = { index, text: ev.text };
393
409
  return;
394
410
  }
411
+ if (ev.type === "artifact" && ev.direction === "out") outboundArtifacts++;
395
412
  // Side facts about the run, not steps: skill_use rides beside a use_skill
396
413
  // call that already produced its own tool pair, and artifact beside the
397
414
  // attach_file call (or the dispatcher's staging) that moved the file;
@@ -582,6 +599,24 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
582
599
  f.interval = { start: at, end: Math.max(at, windowEnd) };
583
600
  }
584
601
  }
602
+ // The reply promised a file the run never produced: no tool failed (a preset without
603
+ // attach_file makes no call), so the answer's own words are the only witness. Once per
604
+ // run, anchored to the answer, no extent; the line that made the promise is quoted.
605
+ if (answer !== undefined && outboundArtifacts === 0) {
606
+ const promised = ATTACHMENT_PROMISE.exec(answer.text);
607
+ if (promised) {
608
+ const line =
609
+ answer.text.slice(0, promised.index).split("\n").pop()! + answer.text.slice(promised.index).split("\n")[0]!;
610
+ const trimmed = line.trim();
611
+ const quoted = trimmed.length > 120 ? `…${trimmed.slice(trimmed.length - 119)}` : trimmed;
612
+ findings.push({
613
+ category: "unkept_promise",
614
+ severity: "low",
615
+ summary: `unkept promise: the reply says a file is attached but the run produced none — "${quoted}"`,
616
+ eventIndex: answer.index,
617
+ });
618
+ }
619
+ }
585
620
 
586
621
  // ---- totals ----------------------------------------------------------------
587
622
  let toolTimeMs: number | undefined;
package/dist/cli.js CHANGED
@@ -1992,6 +1992,8 @@ TIME. Your budget is up to two hours \u2014 less when a boundary or the request'
1992
1992
 
1993
1993
  READ-ONLY: NEVER open a pull request, and never commit or push \u2014 no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:coding\`.
1994
1994
 
1995
+ You cannot attach or post files: your whole answer is text. Never say a file is attached or below \u2014 name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
1996
+
1995
1997
  Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending) once you have it, and update items as they start (\u2731) and finish (\u2713 \u2014 only after they actually happened). Items are short outcomes ("Clone and install", "Time the full suite"), never commands.
1996
1998
 
1997
1999
  ${NOTEPAD}
@@ -13613,6 +13615,8 @@ function analyzeRunFriction(events, opts = {}) {
13613
13615
  let sideFactEvents = 0;
13614
13616
  let spanEvents = 0;
13615
13617
  let wrapUp;
13618
+ let answer;
13619
+ let outboundArtifacts = 0;
13616
13620
  events.forEach((ev, index) => {
13617
13621
  flushTurns(index);
13618
13622
  if (isSpanRecord(ev)) {
@@ -13621,8 +13625,10 @@ function analyzeRunFriction(events, opts = {}) {
13621
13625
  }
13622
13626
  if (isNarrative(ev)) {
13623
13627
  narrativeEvents++;
13628
+ if (ev.type === "answer") answer = { index, text: ev.text };
13624
13629
  return;
13625
13630
  }
13631
+ if (ev.type === "artifact" && ev.direction === "out") outboundArtifacts++;
13626
13632
  if (ev.type === "skill_use" || ev.type === "artifact" || ev.type === "review_artifact" || ev.type === "pr_description" || ev.type === "pr_opened" || ev.type === "review_posted" || ev.type === "ship_round" || ev.type === "route") {
13627
13633
  sideFactEvents++;
13628
13634
  return;
@@ -13768,6 +13774,20 @@ function analyzeRunFriction(events, opts = {}) {
13768
13774
  f.interval = { start: at, end: Math.max(at, windowEnd) };
13769
13775
  }
13770
13776
  }
13777
+ if (answer !== void 0 && outboundArtifacts === 0) {
13778
+ const promised = ATTACHMENT_PROMISE.exec(answer.text);
13779
+ if (promised) {
13780
+ const line3 = answer.text.slice(0, promised.index).split("\n").pop() + answer.text.slice(promised.index).split("\n")[0];
13781
+ const trimmed = line3.trim();
13782
+ const quoted = trimmed.length > 120 ? `\u2026${trimmed.slice(trimmed.length - 119)}` : trimmed;
13783
+ findings.push({
13784
+ category: "unkept_promise",
13785
+ severity: "low",
13786
+ summary: `unkept promise: the reply says a file is attached but the run produced none \u2014 "${quoted}"`,
13787
+ eventIndex: answer.index
13788
+ });
13789
+ }
13790
+ }
13771
13791
  let toolTimeMs;
13772
13792
  for (const s of toolSpans) {
13773
13793
  const d = durationOfSpan(s);
@@ -13844,7 +13864,7 @@ function formatFrictionReport(d) {
13844
13864
  }
13845
13865
  return lines.join("\n");
13846
13866
  }
13847
- var FRICTION_CATEGORIES, CATEGORY_LABEL, DENOMINATOR_OF, DEFAULT_SLOW_TOOL_MS, DEFAULT_SLOW_MODEL_TURN_MS, INSTALL_SEGMENT, QUOTED, callSignature;
13867
+ var FRICTION_CATEGORIES, CATEGORY_LABEL, DENOMINATOR_OF, ATTACHMENT_PROMISE, DEFAULT_SLOW_TOOL_MS, DEFAULT_SLOW_MODEL_TURN_MS, INSTALL_SEGMENT, QUOTED, callSignature;
13848
13868
  var init_runFriction = __esm({
13849
13869
  "../../src/core/runFriction.ts"() {
13850
13870
  "use strict";
@@ -13861,7 +13881,8 @@ var init_runFriction = __esm({
13861
13881
  "setup_install",
13862
13882
  "wrap_up",
13863
13883
  "budget_hit",
13864
- "infra_failure"
13884
+ "infra_failure",
13885
+ "unkept_promise"
13865
13886
  ];
13866
13887
  CATEGORY_LABEL = {
13867
13888
  slow_tool: "slow tool calls",
@@ -13871,7 +13892,8 @@ var init_runFriction = __esm({
13871
13892
  setup_install: "the repo's setup/install",
13872
13893
  wrap_up: "agent wind-down",
13873
13894
  budget_hit: "budget hits",
13874
- infra_failure: "infra failures"
13895
+ infra_failure: "infra failures",
13896
+ unkept_promise: "unkept promises"
13875
13897
  };
13876
13898
  DENOMINATOR_OF = {
13877
13899
  slow_tool: "tool",
@@ -13881,8 +13903,10 @@ var init_runFriction = __esm({
13881
13903
  slow_model_turn: "run",
13882
13904
  wrap_up: "run",
13883
13905
  budget_hit: "run",
13884
- infra_failure: "run"
13906
+ infra_failure: "run",
13907
+ unkept_promise: "run"
13885
13908
  };
13909
+ ATTACHMENT_PROMISE = /\b(?:attached (?:below|here|above)|see (?:the )?attached|i(?:'ve| have) attached|(?:is|are) attached\b|attachments? (?:(?:is|are) )?(?:below|here))/i;
13886
13910
  DEFAULT_SLOW_TOOL_MS = 3e4;
13887
13911
  DEFAULT_SLOW_MODEL_TURN_MS = 6e4;
13888
13912
  INSTALL_SEGMENT = /(?:^|&&|;|\|\|?)\s*(?:sudo\s+)?(?:corepack\s+)?(?:(?:npm|pnpm|bun)\s+(?:install|i|ci|add)(?=\s|$)|yarn(?:\s+(?:install|add)(?=\s|$)|\s*$)|npx\s+playwright\s+install(?=\s|$)|(?:python3?\s+-m\s+)?pip3?\s+install(?=\s|$)|poetry\s+install(?=\s|$)|uv\s+(?:sync|pip\s+install)(?=\s|$)|apt(?:-get)?\s+install(?=\s|$)|apk\s+add(?=\s|$)|brew\s+install(?=\s|$)|bundle\s+install(?=\s|$)|gem\s+install(?=\s|$)|cargo\s+(?:fetch|build)(?=\s|$)|go\s+mod\s+download(?=\s|$)|git\s+clone(?=\s|$)|make\s+(?:deps|install|setup)(?=\s|$))/;
@@ -15030,6 +15054,8 @@ function patternSignature(f) {
15030
15054
  }
15031
15055
  case "wrap_up":
15032
15056
  return "wrap_up:wrap_up";
15057
+ case "unkept_promise":
15058
+ return "unkept_promise:attachment";
15033
15059
  case "slow_model_turn":
15034
15060
  return "slow_model_turn:model_turn";
15035
15061
  case "infra_failure": {
@@ -15249,6 +15275,8 @@ ${evidence}`);
15249
15275
  return `Runs ended with \`${p.signature.replace(/^mid-tool /, "")}\` still outstanding \u2014 the run (or its transport) was cut while the tool ran. Correlate the affected runs with deploys/drains (\`[drain]\` log lines), the sandbox command timeout (exit 124 / heartbeat streaming, docs/reference/specs/execution.md), and the executor's error surfacing.`;
15250
15276
  }
15251
15277
  return `The exec transport failed during \`${p.signature}\` in ${p.runIds.length} runs (the sandbox, not the command). Check the sandbox/resident Worker logs (\`deploy/bin/cf-logs\`) around the affected runs for the underlying error, and whether the command's runtime exceeds the executor's timeout.`;
15278
+ case "unkept_promise":
15279
+ return `In ${p.runIds.length} runs the reply told the person a file was attached and the run produced none \u2014 no outbound \`artifact\` event, so nothing reached the thread or the run page. Read the affected runs' presets: a preset without \`attach_file\` in its toolset (\`src/agents/registry.ts\`) must say in its prompt that its whole answer is text and name the workspace path instead of promising an attachment; an ask that needs the file belongs on a preset with the full toolset (the router's description of the preset should say it cannot post files). If the run did call \`attach_file\` and it failed, the paired \`failed_tool\` proposal has the cause.`;
15252
15280
  case "long_run":
15253
15281
  return `These \`${p.signature}\` runs took \u2265 ${LONG_RUN_FACTOR}\xD7 the median run time \u2014 the closest available proxy for a cost spike (there is no per-run token accounting yet). Open each affected run's friction report for its dominant cause; if the shape of work is legitimately long, split it or lower the agent's effort tier; if it is not, tighten \`maxMinutes\` so a runaway run is cut earlier.`;
15254
15282
  }
@@ -15290,6 +15318,7 @@ var init_frictionProposals = __esm({
15290
15318
  wrap_up: "agent wind-down",
15291
15319
  budget_hit: "budget hit",
15292
15320
  infra_failure: "infra failure",
15321
+ unkept_promise: "unkept promise",
15293
15322
  long_run: "long run"
15294
15323
  };
15295
15324
  RESULT_TAIL = / → [\s\S]*$/;
@@ -23395,7 +23424,7 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs) {
23395
23424
  };
23396
23425
  const resolveAddressed = async (a) => {
23397
23426
  if (a.slug !== void 0) return probe && await vet(a.slug) ? a.slug : void 0;
23398
- const matches = (await listSlugs() ?? []).filter((slug) => slug.split("/")[1] === a.name);
23427
+ const matches = (await listSlugs() ?? []).filter((slug) => slug.split("/")[1]?.toLowerCase() === a.name);
23399
23428
  return matches.length === 1 ? matches[0] : void 0;
23400
23429
  };
23401
23430
  const strongNow = s.pr?.repo ?? (s.repoStrong ? s.repo : void 0) ?? (s.addressed ? await resolveAddressed(s.addressed) : void 0);
@@ -28970,7 +28999,9 @@ function attachmentsLine(outcomes) {
28970
28999
  const parts = [];
28971
29000
  if (landed.length > 0) {
28972
29001
  parts.push(
28973
- `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map((o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type})`).join(", ")}`
29002
+ `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
29003
+ (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
29004
+ ).join(", ")}`
28974
29005
  );
28975
29006
  }
28976
29007
  for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
@@ -29022,9 +29053,63 @@ async function pullStaged(outcomes, deps) {
29022
29053
  }
29023
29054
  return outcomes.map((o) => {
29024
29055
  const error = o.error ?? pulled.get(o.key);
29025
- return error === void 0 ? { file: o.file, basename: o.basename, key: o.key } : { ...o, error };
29056
+ if (error !== void 0) return { ...o, error };
29057
+ const { error: _dropped, ...landedOutcome } = o;
29058
+ return landedOutcome;
29026
29059
  });
29027
29060
  }
29061
+ function threadInboundArtifacts(runsOldestFirst) {
29062
+ const seen = /* @__PURE__ */ new Set();
29063
+ const out = [];
29064
+ for (const events of runsOldestFirst) {
29065
+ for (const e of events) {
29066
+ if (e.type !== "artifact" || e.direction !== "in" || seen.has(e.key)) continue;
29067
+ seen.add(e.key);
29068
+ out.push({ key: e.key, name: e.name, size: e.size, contentType: e.contentType });
29069
+ }
29070
+ }
29071
+ return out;
29072
+ }
29073
+ async function stageThreadArtifacts(artifacts, deps) {
29074
+ const outcomes = [];
29075
+ for (const a of artifacts) {
29076
+ const file = { name: a.name, size: a.size, type: a.contentType };
29077
+ let held;
29078
+ try {
29079
+ held = await deps.store.head(a.key) !== null;
29080
+ } catch (err2) {
29081
+ outcomes.push({
29082
+ file,
29083
+ basename: safeBasename(a.name),
29084
+ key: a.key,
29085
+ earlier: true,
29086
+ error: `the store could not be asked: ${describe4(err2)}`
29087
+ });
29088
+ continue;
29089
+ }
29090
+ if (!held) {
29091
+ outcomes.push({
29092
+ file,
29093
+ basename: safeBasename(a.name),
29094
+ key: a.key,
29095
+ earlier: true,
29096
+ error: "the store no longer holds it (its retention passed)"
29097
+ });
29098
+ continue;
29099
+ }
29100
+ const basename4 = stagedBasename(deps.nextIndex(), a.name);
29101
+ deps.publish?.({
29102
+ type: "artifact",
29103
+ direction: "in",
29104
+ key: a.key,
29105
+ name: a.name,
29106
+ size: a.size,
29107
+ contentType: a.contentType
29108
+ });
29109
+ outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
29110
+ }
29111
+ return outcomes;
29112
+ }
29028
29113
  async function stageIntoWorkspace(files, deps) {
29029
29114
  const outcomes = await pullStaged(await copyStaged(files, deps), deps);
29030
29115
  return { line: attachmentsLine(outcomes), outcomes };
@@ -31254,6 +31339,7 @@ async function runPiHarness(deps, run2) {
31254
31339
  writeUpAt = now();
31255
31340
  transport.send({ type: "steer", message: instruction });
31256
31341
  };
31342
+ let steers = Promise.resolve();
31257
31343
  const drainFollowUps = () => {
31258
31344
  const inputs = run2.inbox?.drain() ?? [];
31259
31345
  if (inputs.length === 0) return;
@@ -31271,7 +31357,26 @@ async function runPiHarness(deps, run2) {
31271
31357
  const images = inputs.flatMap(
31272
31358
  (i) => (i.images ?? []).map((img) => ({ type: "image", data: img.data, mimeType: img.mediaType }))
31273
31359
  );
31274
- transport.send({ type: "steer", message: followUpPrompt(inputs), ...images.length > 0 ? { images } : {} });
31360
+ steers = steers.then(async () => {
31361
+ let stagedLine = "";
31362
+ if (run2.stageFollowUps) {
31363
+ try {
31364
+ stagedLine = await run2.stageFollowUps(inputs);
31365
+ } catch (err2) {
31366
+ stagedLine = `Attached files could not be staged: ${err2 instanceof Error ? err2.message : String(err2)}`;
31367
+ note("follow_up", `staging the follow-up's files failed: ${redactSecrets(stagedLine)}`);
31368
+ }
31369
+ }
31370
+ if (settled) return;
31371
+ const prompt = followUpPrompt(inputs);
31372
+ transport.send({
31373
+ type: "steer",
31374
+ message: stagedLine ? `${prompt}
31375
+
31376
+ ${stagedLine}` : prompt,
31377
+ ...images.length > 0 ? { images } : {}
31378
+ });
31379
+ });
31275
31380
  };
31276
31381
  const check = () => {
31277
31382
  const requested = run2.control?.requested;
@@ -32084,6 +32189,19 @@ async function runLoop2(deps, ctx) {
32084
32189
  const attachFile = io.attachFile?.bind(io);
32085
32190
  const uploadTicket = io.uploadTicket?.bind(io);
32086
32191
  const nextStagedIndex = ctx.stagingIndex ?? stagingIndex();
32192
+ const stageFollowUps = deps.artifacts ? async (inputs) => {
32193
+ const files = inputs.flatMap((i) => i.staged ?? []);
32194
+ if (files.length === 0) return "";
32195
+ const staged = await stageIntoWorkspace(files, {
32196
+ store: deps.artifacts,
32197
+ threadKey: msg.threadKey,
32198
+ publish: (e) => registry.publish(run2.id, e),
32199
+ nextIndex: nextStagedIndex,
32200
+ executor,
32201
+ resident: round2.selection.resident !== void 0
32202
+ });
32203
+ return staged.line;
32204
+ } : void 0;
32087
32205
  let artifactSeq = 0;
32088
32206
  const artifacts = deps.artifacts ? {
32089
32207
  store: deps.artifacts,
@@ -32164,6 +32282,7 @@ async function runLoop2(deps, ctx) {
32164
32282
  span: root,
32165
32283
  control: run2.control,
32166
32284
  inbox: admitted.inbox,
32285
+ ...stageFollowUps ? { stageFollowUps } : {},
32167
32286
  onEvent,
32168
32287
  onProgress,
32169
32288
  ...ledgerRun ? {
@@ -32202,23 +32321,7 @@ async function runLoop2(deps, ctx) {
32202
32321
  // operator stop from /runs
32203
32322
  inbox: admitted.inbox,
32204
32323
  // thread follow-ups steered into this run (thread-admission item 2)
32205
- // A steered follow-up's staged files (record 0033): copied into the
32206
- // store and pulled into this workspace before the model reads the turn.
32207
- ...deps.artifacts ? {
32208
- stageFollowUps: async (inputs) => {
32209
- const files = inputs.flatMap((i) => i.staged ?? []);
32210
- if (files.length === 0) return "";
32211
- const staged = await stageIntoWorkspace(files, {
32212
- store: deps.artifacts,
32213
- threadKey: msg.threadKey,
32214
- publish: (e) => registry.publish(run2.id, e),
32215
- nextIndex: nextStagedIndex,
32216
- executor,
32217
- resident: round2.selection.resident !== void 0
32218
- });
32219
- return staged.line;
32220
- }
32221
- } : {},
32324
+ ...stageFollowUps ? { stageFollowUps } : {},
32222
32325
  // The step record before each step's tools (run-history item 35).
32223
32326
  ...ledgerRun ? { onStep: ledgerRun.step.bind(ledgerRun) } : {},
32224
32327
  // A resume re-enters the loop from the plan (run-history item 37).
@@ -33889,6 +33992,31 @@ var init_lineage = __esm({
33889
33992
  });
33890
33993
 
33891
33994
  // ../../src/core/dispatch/thread.ts
33995
+ async function readThreadArtifacts(service, thread, warn = (line3) => console.warn(line3)) {
33996
+ const perRun = [];
33997
+ for (const run2 of [...thread].reverse()) {
33998
+ const events = [];
33999
+ let afterSeq;
34000
+ try {
34001
+ for (; ; ) {
34002
+ const r = await service.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
34003
+ if (!r.ok) {
34004
+ warn(`[thread] ${run2.id}: reading its received files stopped after ${events.length} event(s) \u2014 ${r.error}`);
34005
+ break;
34006
+ }
34007
+ events.push(...r.value.events);
34008
+ if (r.value.nextAfterSeq === void 0) break;
34009
+ afterSeq = r.value.nextAfterSeq;
34010
+ }
34011
+ } catch (err2) {
34012
+ warn(
34013
+ `[thread] ${run2.id}: reading its received files failed after ${events.length} event(s) \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`
34014
+ );
34015
+ }
34016
+ perRun.push(events);
34017
+ }
34018
+ return threadInboundArtifacts(perRun);
34019
+ }
33892
34020
  async function readThread(service, threadKey) {
33893
34021
  try {
33894
34022
  const page2 = await service.listRuns({
@@ -33923,6 +34051,7 @@ var THREAD_READ_LIMIT;
33923
34051
  var init_thread = __esm({
33924
34052
  "../../src/core/dispatch/thread.ts"() {
33925
34053
  "use strict";
34054
+ init_staging();
33926
34055
  THREAD_READ_LIMIT = 8;
33927
34056
  }
33928
34057
  });
@@ -34401,6 +34530,13 @@ async function dispatch(deps, msg, io, opts = {}) {
34401
34530
  nextIndex: nextStagedIndex,
34402
34531
  publish: (e) => registry.publish(runId3, e)
34403
34532
  }) : void 0;
34533
+ const earlierStaged = !resume && deps.artifacts && agent.machine !== "none" && thread && thread.length > 0 ? readThreadArtifacts(runsService, thread).then(
34534
+ (found) => stageThreadArtifacts(found, {
34535
+ store: deps.artifacts,
34536
+ nextIndex: nextStagedIndex,
34537
+ publish: (e) => registry.publish(runId3, e)
34538
+ })
34539
+ ) : void 0;
34404
34540
  const reservation = await reserveRun(deps, {
34405
34541
  msg,
34406
34542
  agent,
@@ -34450,9 +34586,10 @@ async function dispatch(deps, msg, io, opts = {}) {
34450
34586
  });
34451
34587
  return ended;
34452
34588
  }
34453
- if (staged.length > 0) {
34454
- const line3 = stagedCopies ? attachmentsLine(
34455
- await pullStaged(await stagedCopies, {
34589
+ const earlier2 = earlierStaged ? await earlierStaged : [];
34590
+ if (staged.length > 0 || earlier2.length > 0) {
34591
+ const line3 = stagedCopies || earlier2.length > 0 ? attachmentsLine(
34592
+ await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
34456
34593
  store: deps.artifacts,
34457
34594
  executor,
34458
34595
  resident: resident !== void 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.218.0",
3
+ "version": "1.219.0",
4
4
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://openswitchboard.dev",