runcastle 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/runcastle.js CHANGED
@@ -72,7 +72,7 @@ function resolveSandboxImage(config) {
72
72
  function resolveModel(step, config, project, runOverride) {
73
73
  return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
74
74
  }
75
- var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5-20251001", foldLegacyModelConfig = (raw) => {
75
+ var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5", foldLegacyModelConfig = (raw) => {
76
76
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
77
77
  return raw;
78
78
  const obj = { ...raw };
@@ -99,7 +99,7 @@ var init_config = __esm(() => {
99
99
  ModelStep = z.enum(MODEL_STEPS);
100
100
  RuncastleConfig = z.preprocess(foldLegacyModelConfig, z.object({
101
101
  serverPort: z.number().default(4512),
102
- model: z.string().default("claude-opus-4-8"),
102
+ model: z.string().default("claude-opus-5"),
103
103
  stepModels: z.partialRecord(ModelStep, z.string()).default({ smoke: DEFAULT_SMOKE_MODEL }),
104
104
  sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
105
105
  mainBranch: z.string().default("main"),
@@ -1127,6 +1127,27 @@ var init_errors = __esm(() => {
1127
1127
  };
1128
1128
  });
1129
1129
 
1130
+ // src/services/bus.ts
1131
+ function subscribeLive(fn) {
1132
+ subscribers.add(fn);
1133
+ return () => {
1134
+ subscribers.delete(fn);
1135
+ };
1136
+ }
1137
+ function publishLive(signal) {
1138
+ for (const fn of [...subscribers]) {
1139
+ try {
1140
+ fn(signal);
1141
+ } catch (err) {
1142
+ console.error("live bus subscriber threw", err);
1143
+ }
1144
+ }
1145
+ }
1146
+ var subscribers;
1147
+ var init_bus = __esm(() => {
1148
+ subscribers = new Set;
1149
+ });
1150
+
1130
1151
  // src/services/events.ts
1131
1152
  import { and, asc, eq, gt } from "drizzle-orm";
1132
1153
  function rowToEvent(row) {
@@ -1165,7 +1186,14 @@ function insertEvent(ctx, projectId, featureId, e) {
1165
1186
  message: e.message,
1166
1187
  data: e.data ?? null
1167
1188
  }).returning().get();
1168
- return rowToEvent(row);
1189
+ const event = rowToEvent(row);
1190
+ publishLive({
1191
+ kind: "event",
1192
+ projectId: event.projectId,
1193
+ featureId: event.featureId,
1194
+ eventId: event.id
1195
+ });
1196
+ return event;
1169
1197
  }
1170
1198
  function listAfter(ctx, featureId, afterId = 0) {
1171
1199
  const rows = ctx.db.select().from(events).where(and(eq(events.featureId, featureId), gt(events.id, afterId))).orderBy(asc(events.id)).all();
@@ -1178,6 +1206,7 @@ function listByProject(ctx, projectId, afterId = 0) {
1178
1206
  var init_events = __esm(() => {
1179
1207
  init_schema();
1180
1208
  init_errors();
1209
+ init_bus();
1181
1210
  });
1182
1211
 
1183
1212
  // src/services/repo.ts
@@ -1516,6 +1545,9 @@ function markSessionLive(ctx, id, input = {}) {
1516
1545
  function kickoffLineFor(kind, override) {
1517
1546
  return override ?? KICKOFF_LINES[kind];
1518
1547
  }
1548
+ function resumeKickoffLine(kind) {
1549
+ return RESUME_KICKOFF_PREFIX + KICKOFF_LINES[kind];
1550
+ }
1519
1551
  function setKickoffOverride(sessionId, line) {
1520
1552
  pendingKickoffOverrides.set(sessionId, line);
1521
1553
  }
@@ -1564,15 +1596,15 @@ function markSessionEnded(ctx, id) {
1564
1596
  ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
1565
1597
  return getSessionRow(ctx, id);
1566
1598
  }
1567
- function mostRecentResumableSession(ctx, featureId) {
1568
- const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc2(sql2`rowid`)).limit(1).get();
1599
+ function mostRecentResumableSession(ctx, featureId, kind) {
1600
+ const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId), ...kind ? [eq4(sessions.kind, kind)] : [])).orderBy(desc2(sql2`rowid`)).limit(1).get();
1569
1601
  return row ? rowToSession(row) : null;
1570
1602
  }
1571
1603
  function mostRecentLiveSession(ctx) {
1572
1604
  const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
1573
1605
  return row ? rowToSession(row) : null;
1574
1606
  }
1575
- var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, pendingKickoffOverrides;
1607
+ var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides;
1576
1608
  var init_sessions = __esm(() => {
1577
1609
  init_src();
1578
1610
  init_schema();
@@ -1588,6 +1620,7 @@ var init_sessions = __esm(() => {
1588
1620
  converge: CONVERGE_KICKOFF_LINE,
1589
1621
  revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up."
1590
1622
  };
1623
+ RESUME_KICKOFF_PREFIX = "We are picking this conversation back up \u2014 runcastle restarted and closed the terminal, " + "but this conversation is intact. Do NOT start over: tell me in one or two lines where we " + "left off and what is next, then carry on. Your original instruction was: ";
1591
1624
  pendingKickoffOverrides = new Map;
1592
1625
  });
1593
1626
 
@@ -2686,6 +2719,7 @@ var init_skills_root = __esm(() => {
2686
2719
  // src/services/agent-stream.ts
2687
2720
  function beginTranscript(ticketId) {
2688
2721
  transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
2722
+ publishLive({ kind: "transcript", ticketId });
2689
2723
  }
2690
2724
  function appendTranscript(ticketId, chunk) {
2691
2725
  const t = transcripts.get(ticketId);
@@ -2701,11 +2735,13 @@ function appendTranscript(ticketId, chunk) {
2701
2735
  break;
2702
2736
  t.bytes -= dropped.text.length;
2703
2737
  }
2738
+ publishLive({ kind: "transcript", ticketId });
2704
2739
  }
2705
2740
  function endTranscript(ticketId) {
2706
2741
  const t = transcripts.get(ticketId);
2707
2742
  if (t)
2708
2743
  t.live = false;
2744
+ publishLive({ kind: "transcript", ticketId });
2709
2745
  }
2710
2746
  function readTranscript(ticketId, after = -1) {
2711
2747
  const t = transcripts.get(ticketId);
@@ -2720,6 +2756,7 @@ function readTranscript(ticketId, after = -1) {
2720
2756
  }
2721
2757
  var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
2722
2758
  var init_agent_stream = __esm(() => {
2759
+ init_bus();
2723
2760
  transcripts = new Map;
2724
2761
  });
2725
2762
 
@@ -4632,6 +4669,57 @@ var init_hooks = __esm(() => {
4632
4669
  hooks_default = hooks;
4633
4670
  });
4634
4671
 
4672
+ // src/routes/stream.ts
4673
+ import { Hono as Hono3 } from "hono";
4674
+ import { streamSSE } from "hono/streaming";
4675
+ function dedupeKey(s) {
4676
+ return s.kind === "transcript" ? `transcript:${s.ticketId}` : "event";
4677
+ }
4678
+ var stream, FLUSH_MS = 120, HEARTBEAT_MS = 25000, stream_default;
4679
+ var init_stream = __esm(() => {
4680
+ init_bus();
4681
+ stream = new Hono3;
4682
+ stream.get("/", (c) => streamSSE(c, async (sse) => {
4683
+ const pending = new Map;
4684
+ let done = false;
4685
+ const stop = () => {
4686
+ done = true;
4687
+ };
4688
+ const unsubscribe = subscribeLive((signal) => {
4689
+ pending.set(dedupeKey(signal), signal);
4690
+ });
4691
+ sse.onAbort(stop);
4692
+ c.req.raw.signal.addEventListener("abort", stop);
4693
+ try {
4694
+ await sse.writeSSE({ event: "ready", data: "{}" });
4695
+ let idleMs = 0;
4696
+ while (!done && !sse.aborted && !sse.closed) {
4697
+ await sse.sleep(FLUSH_MS);
4698
+ if (done || sse.aborted || sse.closed)
4699
+ break;
4700
+ if (pending.size === 0) {
4701
+ idleMs += FLUSH_MS;
4702
+ if (idleMs >= HEARTBEAT_MS) {
4703
+ idleMs = 0;
4704
+ await sse.writeSSE({ event: "ping", data: "{}" });
4705
+ }
4706
+ continue;
4707
+ }
4708
+ idleMs = 0;
4709
+ const batch = [...pending.values()];
4710
+ pending.clear();
4711
+ for (const signal of batch) {
4712
+ await sse.writeSSE({ event: "live", data: JSON.stringify(signal) });
4713
+ }
4714
+ }
4715
+ } finally {
4716
+ unsubscribe();
4717
+ c.req.raw.signal.removeEventListener("abort", stop);
4718
+ }
4719
+ }));
4720
+ stream_default = stream;
4721
+ });
4722
+
4635
4723
  // src/routes/web.ts
4636
4724
  import { existsSync as existsSync12, statSync } from "fs";
4637
4725
  import { readFile } from "fs/promises";
@@ -5124,8 +5212,6 @@ async function launchSession(ctx, input, opts = {}) {
5124
5212
  kind: input.kind,
5125
5213
  worktreePath
5126
5214
  });
5127
- if (input.kickoffLine)
5128
- setKickoffOverride(session.id, input.kickoffLine);
5129
5215
  let waypoint;
5130
5216
  let resumeSessionId;
5131
5217
  let resumeUnavailableFrom;
@@ -5158,11 +5244,24 @@ async function launchSession(ctx, input, opts = {}) {
5158
5244
  resumeUnavailableFrom = "revisit";
5159
5245
  }
5160
5246
  }
5247
+ if (input.kind !== "waypoint" && input.kind !== "revisit") {
5248
+ resumeSessionId = mostRecentResumableSession(ctx, feature.id, input.kind)?.ccSessionId;
5249
+ }
5250
+ const kickoffLine = input.kickoffLine ?? (resumeSessionId ? resumeKickoffLine(input.kind) : undefined);
5251
+ if (kickoffLine)
5252
+ setKickoffOverride(session.id, kickoffLine);
5161
5253
  emit(ctx, feature.id, {
5162
5254
  type: "session.launching",
5163
5255
  message: `launching ${input.kind} session`,
5164
5256
  data: { sessionId: session.id, kind: input.kind, worktreePath, waypointId: waypoint?.id }
5165
5257
  });
5258
+ if (resumeSessionId) {
5259
+ emit(ctx, feature.id, {
5260
+ type: "session.resumed",
5261
+ message: `resuming the previous ${input.kind} conversation`,
5262
+ data: { sessionId: session.id, kind: input.kind, resumeSessionId }
5263
+ });
5264
+ }
5166
5265
  if (input.kind === "revisit" && resumeUnavailableFrom) {
5167
5266
  emit(ctx, feature.id, {
5168
5267
  type: "session.resume_unavailable",
@@ -6595,9 +6694,9 @@ __export(exports_src, {
6595
6694
  buildApp: () => buildApp
6596
6695
  });
6597
6696
  import { trpcServer } from "@hono/trpc-server";
6598
- import { Hono as Hono3 } from "hono";
6697
+ import { Hono as Hono4 } from "hono";
6599
6698
  function buildApp(ctx) {
6600
- const app = new Hono3;
6699
+ const app = new Hono4;
6601
6700
  setRuntimeCtx(ctx);
6602
6701
  app.get("/health", (c) => c.json({ ok: true }));
6603
6702
  app.use("/api/trpc/*", async (c, next) => {
@@ -6611,6 +6710,7 @@ function buildApp(ctx) {
6611
6710
  createContext: () => ctx
6612
6711
  }));
6613
6712
  app.route("/api/hooks", hooks_default);
6713
+ app.route("/api/stream", stream_default);
6614
6714
  app.route("/mcp", server_default);
6615
6715
  mountWebAppIfBuilt(app);
6616
6716
  return app;
@@ -6663,6 +6763,7 @@ var init_src2 = __esm(() => {
6663
6763
  init_registry();
6664
6764
  init_ws();
6665
6765
  init_hooks();
6766
+ init_stream();
6666
6767
  init_web();
6667
6768
  init_router();
6668
6769
  init_reconcile_runs();
package/index.js CHANGED
@@ -71,7 +71,7 @@ function resolveSandboxImage(config) {
71
71
  function resolveModel(step, config, project, runOverride) {
72
72
  return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
73
73
  }
74
- var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5-20251001", foldLegacyModelConfig = (raw) => {
74
+ var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5", foldLegacyModelConfig = (raw) => {
75
75
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
76
76
  return raw;
77
77
  const obj = { ...raw };
@@ -98,7 +98,7 @@ var init_config = __esm(() => {
98
98
  ModelStep = z.enum(MODEL_STEPS);
99
99
  RuncastleConfig = z.preprocess(foldLegacyModelConfig, z.object({
100
100
  serverPort: z.number().default(4512),
101
- model: z.string().default("claude-opus-4-8"),
101
+ model: z.string().default("claude-opus-5"),
102
102
  stepModels: z.partialRecord(ModelStep, z.string()).default({ smoke: DEFAULT_SMOKE_MODEL }),
103
103
  sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
104
104
  mainBranch: z.string().default("main"),
@@ -1126,6 +1126,27 @@ var init_errors = __esm(() => {
1126
1126
  };
1127
1127
  });
1128
1128
 
1129
+ // src/services/bus.ts
1130
+ function subscribeLive(fn) {
1131
+ subscribers.add(fn);
1132
+ return () => {
1133
+ subscribers.delete(fn);
1134
+ };
1135
+ }
1136
+ function publishLive(signal) {
1137
+ for (const fn of [...subscribers]) {
1138
+ try {
1139
+ fn(signal);
1140
+ } catch (err) {
1141
+ console.error("live bus subscriber threw", err);
1142
+ }
1143
+ }
1144
+ }
1145
+ var subscribers;
1146
+ var init_bus = __esm(() => {
1147
+ subscribers = new Set;
1148
+ });
1149
+
1129
1150
  // src/services/events.ts
1130
1151
  import { and, asc, eq, gt } from "drizzle-orm";
1131
1152
  function rowToEvent(row) {
@@ -1164,7 +1185,14 @@ function insertEvent(ctx, projectId, featureId, e) {
1164
1185
  message: e.message,
1165
1186
  data: e.data ?? null
1166
1187
  }).returning().get();
1167
- return rowToEvent(row);
1188
+ const event = rowToEvent(row);
1189
+ publishLive({
1190
+ kind: "event",
1191
+ projectId: event.projectId,
1192
+ featureId: event.featureId,
1193
+ eventId: event.id
1194
+ });
1195
+ return event;
1168
1196
  }
1169
1197
  function listAfter(ctx, featureId, afterId = 0) {
1170
1198
  const rows = ctx.db.select().from(events).where(and(eq(events.featureId, featureId), gt(events.id, afterId))).orderBy(asc(events.id)).all();
@@ -1177,6 +1205,7 @@ function listByProject(ctx, projectId, afterId = 0) {
1177
1205
  var init_events = __esm(() => {
1178
1206
  init_schema();
1179
1207
  init_errors();
1208
+ init_bus();
1180
1209
  });
1181
1210
 
1182
1211
  // src/services/repo.ts
@@ -1515,6 +1544,9 @@ function markSessionLive(ctx, id, input = {}) {
1515
1544
  function kickoffLineFor(kind, override) {
1516
1545
  return override ?? KICKOFF_LINES[kind];
1517
1546
  }
1547
+ function resumeKickoffLine(kind) {
1548
+ return RESUME_KICKOFF_PREFIX + KICKOFF_LINES[kind];
1549
+ }
1518
1550
  function setKickoffOverride(sessionId, line) {
1519
1551
  pendingKickoffOverrides.set(sessionId, line);
1520
1552
  }
@@ -1563,15 +1595,15 @@ function markSessionEnded(ctx, id) {
1563
1595
  ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
1564
1596
  return getSessionRow(ctx, id);
1565
1597
  }
1566
- function mostRecentResumableSession(ctx, featureId) {
1567
- const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc2(sql2`rowid`)).limit(1).get();
1598
+ function mostRecentResumableSession(ctx, featureId, kind) {
1599
+ const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId), ...kind ? [eq4(sessions.kind, kind)] : [])).orderBy(desc2(sql2`rowid`)).limit(1).get();
1568
1600
  return row ? rowToSession(row) : null;
1569
1601
  }
1570
1602
  function mostRecentLiveSession(ctx) {
1571
1603
  const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
1572
1604
  return row ? rowToSession(row) : null;
1573
1605
  }
1574
- var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, pendingKickoffOverrides;
1606
+ var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides;
1575
1607
  var init_sessions = __esm(() => {
1576
1608
  init_src();
1577
1609
  init_schema();
@@ -1587,6 +1619,7 @@ var init_sessions = __esm(() => {
1587
1619
  converge: CONVERGE_KICKOFF_LINE,
1588
1620
  revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up."
1589
1621
  };
1622
+ RESUME_KICKOFF_PREFIX = "We are picking this conversation back up \u2014 runcastle restarted and closed the terminal, " + "but this conversation is intact. Do NOT start over: tell me in one or two lines where we " + "left off and what is next, then carry on. Your original instruction was: ";
1590
1623
  pendingKickoffOverrides = new Map;
1591
1624
  });
1592
1625
 
@@ -2685,6 +2718,7 @@ var init_skills_root = __esm(() => {
2685
2718
  // src/services/agent-stream.ts
2686
2719
  function beginTranscript(ticketId) {
2687
2720
  transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
2721
+ publishLive({ kind: "transcript", ticketId });
2688
2722
  }
2689
2723
  function appendTranscript(ticketId, chunk) {
2690
2724
  const t = transcripts.get(ticketId);
@@ -2700,11 +2734,13 @@ function appendTranscript(ticketId, chunk) {
2700
2734
  break;
2701
2735
  t.bytes -= dropped.text.length;
2702
2736
  }
2737
+ publishLive({ kind: "transcript", ticketId });
2703
2738
  }
2704
2739
  function endTranscript(ticketId) {
2705
2740
  const t = transcripts.get(ticketId);
2706
2741
  if (t)
2707
2742
  t.live = false;
2743
+ publishLive({ kind: "transcript", ticketId });
2708
2744
  }
2709
2745
  function readTranscript(ticketId, after = -1) {
2710
2746
  const t = transcripts.get(ticketId);
@@ -2719,6 +2755,7 @@ function readTranscript(ticketId, after = -1) {
2719
2755
  }
2720
2756
  var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
2721
2757
  var init_agent_stream = __esm(() => {
2758
+ init_bus();
2722
2759
  transcripts = new Map;
2723
2760
  });
2724
2761
 
@@ -4631,6 +4668,57 @@ var init_hooks = __esm(() => {
4631
4668
  hooks_default = hooks;
4632
4669
  });
4633
4670
 
4671
+ // src/routes/stream.ts
4672
+ import { Hono as Hono3 } from "hono";
4673
+ import { streamSSE } from "hono/streaming";
4674
+ function dedupeKey(s) {
4675
+ return s.kind === "transcript" ? `transcript:${s.ticketId}` : "event";
4676
+ }
4677
+ var stream, FLUSH_MS = 120, HEARTBEAT_MS = 25000, stream_default;
4678
+ var init_stream = __esm(() => {
4679
+ init_bus();
4680
+ stream = new Hono3;
4681
+ stream.get("/", (c) => streamSSE(c, async (sse) => {
4682
+ const pending = new Map;
4683
+ let done = false;
4684
+ const stop = () => {
4685
+ done = true;
4686
+ };
4687
+ const unsubscribe = subscribeLive((signal) => {
4688
+ pending.set(dedupeKey(signal), signal);
4689
+ });
4690
+ sse.onAbort(stop);
4691
+ c.req.raw.signal.addEventListener("abort", stop);
4692
+ try {
4693
+ await sse.writeSSE({ event: "ready", data: "{}" });
4694
+ let idleMs = 0;
4695
+ while (!done && !sse.aborted && !sse.closed) {
4696
+ await sse.sleep(FLUSH_MS);
4697
+ if (done || sse.aborted || sse.closed)
4698
+ break;
4699
+ if (pending.size === 0) {
4700
+ idleMs += FLUSH_MS;
4701
+ if (idleMs >= HEARTBEAT_MS) {
4702
+ idleMs = 0;
4703
+ await sse.writeSSE({ event: "ping", data: "{}" });
4704
+ }
4705
+ continue;
4706
+ }
4707
+ idleMs = 0;
4708
+ const batch = [...pending.values()];
4709
+ pending.clear();
4710
+ for (const signal of batch) {
4711
+ await sse.writeSSE({ event: "live", data: JSON.stringify(signal) });
4712
+ }
4713
+ }
4714
+ } finally {
4715
+ unsubscribe();
4716
+ c.req.raw.signal.removeEventListener("abort", stop);
4717
+ }
4718
+ }));
4719
+ stream_default = stream;
4720
+ });
4721
+
4634
4722
  // src/routes/web.ts
4635
4723
  import { existsSync as existsSync12, statSync } from "fs";
4636
4724
  import { readFile } from "fs/promises";
@@ -5123,8 +5211,6 @@ async function launchSession(ctx, input, opts = {}) {
5123
5211
  kind: input.kind,
5124
5212
  worktreePath
5125
5213
  });
5126
- if (input.kickoffLine)
5127
- setKickoffOverride(session.id, input.kickoffLine);
5128
5214
  let waypoint;
5129
5215
  let resumeSessionId;
5130
5216
  let resumeUnavailableFrom;
@@ -5157,11 +5243,24 @@ async function launchSession(ctx, input, opts = {}) {
5157
5243
  resumeUnavailableFrom = "revisit";
5158
5244
  }
5159
5245
  }
5246
+ if (input.kind !== "waypoint" && input.kind !== "revisit") {
5247
+ resumeSessionId = mostRecentResumableSession(ctx, feature.id, input.kind)?.ccSessionId;
5248
+ }
5249
+ const kickoffLine = input.kickoffLine ?? (resumeSessionId ? resumeKickoffLine(input.kind) : undefined);
5250
+ if (kickoffLine)
5251
+ setKickoffOverride(session.id, kickoffLine);
5160
5252
  emit(ctx, feature.id, {
5161
5253
  type: "session.launching",
5162
5254
  message: `launching ${input.kind} session`,
5163
5255
  data: { sessionId: session.id, kind: input.kind, worktreePath, waypointId: waypoint?.id }
5164
5256
  });
5257
+ if (resumeSessionId) {
5258
+ emit(ctx, feature.id, {
5259
+ type: "session.resumed",
5260
+ message: `resuming the previous ${input.kind} conversation`,
5261
+ data: { sessionId: session.id, kind: input.kind, resumeSessionId }
5262
+ });
5263
+ }
5165
5264
  if (input.kind === "revisit" && resumeUnavailableFrom) {
5166
5265
  emit(ctx, feature.id, {
5167
5266
  type: "session.resume_unavailable",
@@ -6594,9 +6693,9 @@ __export(exports_src, {
6594
6693
  buildApp: () => buildApp
6595
6694
  });
6596
6695
  import { trpcServer } from "@hono/trpc-server";
6597
- import { Hono as Hono3 } from "hono";
6696
+ import { Hono as Hono4 } from "hono";
6598
6697
  function buildApp(ctx) {
6599
- const app = new Hono3;
6698
+ const app = new Hono4;
6600
6699
  setRuntimeCtx(ctx);
6601
6700
  app.get("/health", (c) => c.json({ ok: true }));
6602
6701
  app.use("/api/trpc/*", async (c, next) => {
@@ -6610,6 +6709,7 @@ function buildApp(ctx) {
6610
6709
  createContext: () => ctx
6611
6710
  }));
6612
6711
  app.route("/api/hooks", hooks_default);
6712
+ app.route("/api/stream", stream_default);
6613
6713
  app.route("/mcp", server_default);
6614
6714
  mountWebAppIfBuilt(app);
6615
6715
  return app;
@@ -6662,6 +6762,7 @@ var init_src2 = __esm(() => {
6662
6762
  init_registry();
6663
6763
  init_ws();
6664
6764
  init_hooks();
6765
+ init_stream();
6665
6766
  init_web();
6666
6767
  init_router();
6667
6768
  init_reconcile_runs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",