@codeworksh/harness 0.4.0-dev.20261020131429 → 0.5.0-canary.55.1.g9147d546

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/effect.mjs CHANGED
@@ -44,6 +44,7 @@ var schema_exports$8 = /* @__PURE__ */ __exportAll({
44
44
  CompactionData: () => CompactionData,
45
45
  Config: () => Config$1,
46
46
  DEFAULT_TITLE: () => DEFAULT_TITLE,
47
+ Execution: () => Execution,
47
48
  ID: () => ID$1,
48
49
  IDFromDb: () => IDFromDb,
49
50
  InterruptReason: () => InterruptReason,
@@ -88,6 +89,12 @@ const Config$1 = Schema.Struct({
88
89
  * different facts, and inspecting the cause cannot tell them apart.
89
90
  */
90
91
  const InterruptReason = Schema.Literals(["user", "shutdown"]);
92
+ /**
93
+ * A run left unfinished: `running` while one is in flight, and still after the process died during
94
+ * it; `closed` once a shutdown stopped it. Either resumes when the server starts again; a run that
95
+ * ended any other way leaves none.
96
+ */
97
+ const Execution = Schema.Literals(["running", "closed"]);
91
98
  const JsonObject = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown));
92
99
  const MessageEnvelopeIdentity = Schema.fromJsonString(Schema.Struct({ messageId: Schema.String }));
93
100
  const CompactionData = Schema.fromJsonString(Schema.Struct({
@@ -368,6 +375,14 @@ const migrations = {
368
375
  }),
369
376
  "202609290001_session_config": Effect.gen(function* () {
370
377
  yield* (yield* SqlClient.SqlClient)`ALTER TABLE session ADD COLUMN config TEXT`;
378
+ }),
379
+ "202610230001_session_input_cancel": Effect.gen(function* () {
380
+ yield* (yield* SqlClient.SqlClient)`ALTER TABLE session_input ADD COLUMN cancelled_seq INTEGER`;
381
+ }),
382
+ "202610240001_session_execution": Effect.gen(function* () {
383
+ const sql = yield* SqlClient.SqlClient;
384
+ yield* sql`ALTER TABLE session ADD COLUMN execution TEXT`;
385
+ yield* sql`ALTER TABLE session ADD COLUMN restarts INTEGER NOT NULL DEFAULT 0`;
371
386
  })
372
387
  };
373
388
  //#endregion
@@ -591,6 +606,8 @@ var SessionRow = class extends Model$1.Class("SessionRow")({
591
606
  tokensOutput: Model$1.GeneratedByDb(Schema.Int),
592
607
  tokensCacheRead: Model$1.GeneratedByDb(Schema.Int),
593
608
  tokensCacheWrite: Model$1.GeneratedByDb(Schema.Int),
609
+ execution: Model$1.FieldOption(Execution),
610
+ restarts: Model$1.GeneratedByDb(Schema.Int),
594
611
  ...Timestamps
595
612
  }) {};
596
613
  var SessionInputRow = class extends Model$1.Class("SessionInputRow")({
@@ -600,6 +617,7 @@ var SessionInputRow = class extends Model$1.Class("SessionInputRow")({
600
617
  delivery: Schema.Literals(inputDeliveries),
601
618
  admittedSeq: Schema.Int,
602
619
  promotedSeq: Model$1.FieldOption(Schema.Int),
620
+ cancelledSeq: Model$1.FieldOption(Schema.Int),
603
621
  createdAt: Model$1.DateTimeInsertFromNumber
604
622
  }) {};
605
623
  var SessionEntryRow = class extends Model$1.Class("SessionEntryRow")({
@@ -731,6 +749,8 @@ var list_exports = /* @__PURE__ */ __exportAll({
731
749
  LLMThinkingStart: () => LLMThinkingStart,
732
750
  PluginUpdated: () => PluginUpdated,
733
751
  PromptAdmitted: () => PromptAdmitted,
752
+ PromptCancelled: () => PromptCancelled,
753
+ PromptUpdated: () => PromptUpdated,
734
754
  Prompted: () => Prompted,
735
755
  PublicDefinitions: () => PublicDefinitions,
736
756
  RetryFinished: () => RetryFinished,
@@ -768,6 +788,29 @@ const Prompted = schema_exports$7.EventSchema.define({
768
788
  ...durableOptions,
769
789
  schema: PromptFields
770
790
  });
791
+ /** A pending prompt withdrawn before promotion; it never enters the conversation. */
792
+ const PromptCancelled = schema_exports$7.EventSchema.define({
793
+ type: "session.prompt.cancelled",
794
+ ...durableOptions,
795
+ schema: {
796
+ ...baseOptions,
797
+ messageId: ID
798
+ }
799
+ });
800
+ /**
801
+ * A pending prompt moved to the other lane or rewritten before promotion; it keeps its place. Only
802
+ * what changed is carried, so two updates of different fields both stand.
803
+ */
804
+ const PromptUpdated = schema_exports$7.EventSchema.define({
805
+ type: "session.prompt.updated",
806
+ ...durableOptions,
807
+ schema: {
808
+ ...baseOptions,
809
+ messageId: ID,
810
+ prompt: Prompt.pipe(schema_exports$9.optional),
811
+ delivery: Delivery.pipe(schema_exports$9.optional)
812
+ }
813
+ });
771
814
  const ConfigChanged = schema_exports$7.EventSchema.define({
772
815
  type: "session.config.changed",
773
816
  ...durableOptions,
@@ -1002,14 +1045,14 @@ const PluginUpdated = schema_exports$7.EventSchema.define({
1002
1045
  * Everything the kernel can publish. `PublicDefinitions` is the narrower set
1003
1046
  * that reaches clients -- adding an event here does not publish it.
1004
1047
  */
1005
- const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnStarted, TurnEnded, TurnAborted, RetryScheduled, RetryFinished, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
1048
+ const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, PromptCancelled, PromptUpdated, ConfigChanged, SessionForked, TurnStarted, TurnEnded, TurnAborted, RetryScheduled, RetryFinished, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
1006
1049
  /**
1007
1050
  * The wire contract: what a client is allowed to receive. Identical to
1008
1051
  * `Definitions` today, and separate so that adding a kernel event is not by
1009
1052
  * itself a decision to publish it.
1010
1053
  */
1011
1054
  const PublicDefinitions = Definitions;
1012
- const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
1055
+ const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, PromptCancelled, PromptUpdated, ConfigChanged, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
1013
1056
  const Manifest$2 = {
1014
1057
  definitions: schema_exports$7.EventSchema.durable([...DurableDefinitions]),
1015
1058
  schema: Schema.Union([...DurableDefinitions], { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
@@ -1807,6 +1850,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1807
1850
  directory: input.directory,
1808
1851
  hostDir: Option.fromUndefinedOr(input.hostDir),
1809
1852
  config: Option.none(),
1853
+ execution: Option.none(),
1810
1854
  title: input.title,
1811
1855
  tag: Option.fromUndefinedOr(input.tag),
1812
1856
  metadata: Option.fromUndefinedOr(input.metadata),
@@ -2160,6 +2204,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2160
2204
  directory: source.value.directory,
2161
2205
  hostDir: source.value.hostDir,
2162
2206
  config: source.value.config,
2207
+ execution: Option.none(),
2163
2208
  title: input.title ?? source.value.title,
2164
2209
  tag: input.tag === void 0 ? source.value.tag : Option.some(input.tag),
2165
2210
  metadata: source.value.metadata,
@@ -2269,6 +2314,41 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2269
2314
  WHERE id = ${sessionId}
2270
2315
  `;
2271
2316
  }, Effect.orDie);
2317
+ const markRunning = Effect.fn("Session.markRunning")(function* (sessionId) {
2318
+ yield* sql`UPDATE session SET execution = 'running' WHERE id = ${sessionId}`;
2319
+ }, Effect.orDie);
2320
+ const endExecution = Effect.fn("Session.endExecution")(function* (sessionId, end, stoppedAt) {
2321
+ if (end === "closed") {
2322
+ yield* sql`UPDATE session SET execution = 'closed' WHERE id = ${sessionId}`;
2323
+ return;
2324
+ }
2325
+ const after = end === "stopped" ? stoppedAt ?? Number.MAX_SAFE_INTEGER : -1;
2326
+ yield* sql`
2327
+ UPDATE session SET execution = NULL, restarts = 0
2328
+ WHERE id = ${sessionId} AND NOT EXISTS (
2329
+ SELECT 1 FROM session_input
2330
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
2331
+ AND admitted_seq > ${after}
2332
+ )
2333
+ `;
2334
+ }, Effect.orDie);
2335
+ const claimRestart = Effect.fn("Session.claimRestart")(function* (sessionId) {
2336
+ const rows = yield* sql`
2337
+ UPDATE session
2338
+ SET restarts = restarts + (CASE WHEN execution = 'running' THEN 1 ELSE 0 END), execution = 'running'
2339
+ WHERE id = ${sessionId} AND execution IS NOT NULL
2340
+ RETURNING restarts
2341
+ `;
2342
+ return Option.fromUndefinedOr(rows[0]?.restarts);
2343
+ }, Effect.orDie);
2344
+ const resetRestarts = Effect.fn("Session.resetRestarts")(function* (sessionId) {
2345
+ yield* sql`UPDATE session SET restarts = 0 WHERE id = ${sessionId} AND restarts > 0`;
2346
+ }, Effect.orDie);
2347
+ const unfinished = SqlSchema.findAll({
2348
+ Request: Schema.Void,
2349
+ Result: SessionRow,
2350
+ execute: () => sql`SELECT * FROM session WHERE execution IS NOT NULL ORDER BY updated_at`
2351
+ })(void 0).pipe(Effect.orDie, Effect.withSpan("Session.unfinished"));
2272
2352
  const relink = Effect.fn("Session.relink")(function* (input) {
2273
2353
  const reject = (reason) => new RelinkError({
2274
2354
  sessionId: input.sessionId,
@@ -2330,6 +2410,11 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2330
2410
  link,
2331
2411
  retitle,
2332
2412
  configure,
2413
+ markRunning,
2414
+ endExecution,
2415
+ claimRestart,
2416
+ resetRestarts,
2417
+ unfinished,
2333
2418
  get,
2334
2419
  space,
2335
2420
  project,
@@ -4972,6 +5057,7 @@ var Service$6 = class extends Context.Service()("@codeworksh/harness/runner/exec
4972
5057
  Layer.succeed(Service$6, Service$6.of({
4973
5058
  active: Effect.succeed(/* @__PURE__ */ new Set()),
4974
5059
  resume: () => Effect.void,
5060
+ recover: () => Effect.void,
4975
5061
  wake: () => Effect.void,
4976
5062
  interrupt: () => Effect.succeed(false),
4977
5063
  awaitIdle: () => Effect.void
@@ -4985,7 +5071,8 @@ const Admitted = Schema.Struct({
4985
5071
  prompt: Prompt,
4986
5072
  delivery: Delivery,
4987
5073
  timeCreated: schema_exports$9.DateTimeUtcFromMillis,
4988
- promotedSeq: schema_exports$9.NonNegativeInt.pipe(schema_exports$9.optional)
5074
+ promotedSeq: schema_exports$9.NonNegativeInt.pipe(schema_exports$9.optional),
5075
+ cancelledSeq: schema_exports$9.NonNegativeInt.pipe(schema_exports$9.optional)
4989
5076
  }).annotate({ identifier: "SessionInput.Admitted" });
4990
5077
  //#endregion
4991
5078
  //#region src/session/input/input.ts
@@ -5006,7 +5093,8 @@ const fromRow = (row) => Admitted.make({
5006
5093
  prompt: row.prompt,
5007
5094
  delivery: row.delivery,
5008
5095
  timeCreated: row.createdAt,
5009
- ...Option.isNone(row.promotedSeq) ? {} : { promotedSeq: row.promotedSeq.value }
5096
+ ...Option.isNone(row.promotedSeq) ? {} : { promotedSeq: row.promotedSeq.value },
5097
+ ...Option.isNone(row.cancelledSeq) ? {} : { cancelledSeq: row.cancelledSeq.value }
5010
5098
  });
5011
5099
  /**
5012
5100
  * Whether a stored input carries the prompt the caller believes it admitted.
@@ -5030,6 +5118,38 @@ const make$8 = Effect.gen(function* () {
5030
5118
  INSERT INTO session_input ${sql.insert(row)}
5031
5119
  ON CONFLICT(id) DO NOTHING
5032
5120
  RETURNING id
5121
+ `
5122
+ });
5123
+ const updateRow = SqlSchema.findAll({
5124
+ Request: Schema.Struct({
5125
+ id: Schema.String,
5126
+ sessionId: Schema.String,
5127
+ prompt: Schema.NullOr(Schema.fromJsonString(Prompt)),
5128
+ delivery: Schema.NullOr(Delivery)
5129
+ }),
5130
+ Result: Schema.Struct({ id: Schema.String }),
5131
+ execute: ({ id, sessionId, prompt, delivery }) => sql`
5132
+ UPDATE session_input SET prompt = COALESCE(${prompt}, prompt), delivery = COALESCE(${delivery}, delivery)
5133
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5134
+ RETURNING id
5135
+ `
5136
+ });
5137
+ const reviveRow = SqlSchema.findAll({
5138
+ Request: Schema.Struct({
5139
+ id: Schema.String,
5140
+ sessionId: Schema.String,
5141
+ prompt: Schema.fromJsonString(Prompt),
5142
+ delivery: Delivery,
5143
+ admittedSeq: Schema.Int,
5144
+ createdAt: schema_exports$9.DateTimeUtcFromMillis
5145
+ }),
5146
+ Result: Schema.Struct({ id: Schema.String }),
5147
+ execute: ({ id, sessionId, prompt, delivery, admittedSeq, createdAt }) => sql`
5148
+ UPDATE session_input
5149
+ SET prompt = ${prompt}, delivery = ${delivery}, admitted_seq = ${admittedSeq}, created_at = ${createdAt},
5150
+ cancelled_seq = NULL
5151
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NOT NULL
5152
+ RETURNING id
5033
5153
  `
5034
5154
  });
5035
5155
  const promoteRow = SqlSchema.findAll({
@@ -5041,8 +5161,21 @@ const make$8 = Effect.gen(function* () {
5041
5161
  Result: SessionInputRow,
5042
5162
  execute: ({ id, sessionId, promotedSeq }) => sql`
5043
5163
  UPDATE session_input SET promoted_seq = ${promotedSeq}
5044
- WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL
5164
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5045
5165
  RETURNING *
5166
+ `
5167
+ });
5168
+ const cancelRow = SqlSchema.findAll({
5169
+ Request: Schema.Struct({
5170
+ id: Schema.String,
5171
+ sessionId: Schema.String,
5172
+ cancelledSeq: Schema.Int
5173
+ }),
5174
+ Result: Schema.Struct({ id: Schema.String }),
5175
+ execute: ({ id, sessionId, cancelledSeq }) => sql`
5176
+ UPDATE session_input SET cancelled_seq = ${cancelledSeq}
5177
+ WHERE id = ${id} AND session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5178
+ RETURNING id
5046
5179
  `
5047
5180
  });
5048
5181
  const selectEntry = SqlSchema.findAll({
@@ -5058,8 +5191,17 @@ const make$8 = Effect.gen(function* () {
5058
5191
  Result: Schema.Struct({ id: Schema.String }),
5059
5192
  execute: ({ sessionId, delivery }) => sql`
5060
5193
  SELECT id FROM session_input
5061
- WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND delivery = ${delivery}
5194
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL AND delivery = ${delivery}
5062
5195
  LIMIT 1
5196
+ `
5197
+ });
5198
+ const selectPending = SqlSchema.findAll({
5199
+ Request: Schema.String,
5200
+ Result: SessionInputRow,
5201
+ execute: (sessionId) => sql`
5202
+ SELECT * FROM session_input
5203
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL
5204
+ ORDER BY admitted_seq
5063
5205
  `
5064
5206
  });
5065
5207
  const selectPendingSteers = SqlSchema.findAll({
@@ -5072,6 +5214,7 @@ const make$8 = Effect.gen(function* () {
5072
5214
  SELECT * FROM session_input
5073
5215
  WHERE session_id = ${sessionId}
5074
5216
  AND promoted_seq IS NULL
5217
+ AND cancelled_seq IS NULL
5075
5218
  AND delivery = 'steer'
5076
5219
  AND admitted_seq <= ${cutoff}
5077
5220
  ORDER BY admitted_seq
@@ -5082,7 +5225,7 @@ const make$8 = Effect.gen(function* () {
5082
5225
  Result: SessionInputRow,
5083
5226
  execute: (sessionId) => sql`
5084
5227
  SELECT * FROM session_input
5085
- WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND delivery = 'followUp'
5228
+ WHERE session_id = ${sessionId} AND promoted_seq IS NULL AND cancelled_seq IS NULL AND delivery = 'followUp'
5086
5229
  ORDER BY admitted_seq
5087
5230
  LIMIT 1
5088
5231
  `
@@ -5092,7 +5235,7 @@ const make$8 = Effect.gen(function* () {
5092
5235
  }, Effect.orDie);
5093
5236
  const admit = Effect.fn("SessionInput.admit")(function* (input) {
5094
5237
  const existing = yield* find(input.id);
5095
- if (Option.isSome(existing)) return existing.value;
5238
+ if (Option.isSome(existing) && existing.value.cancelledSeq === void 0) return existing.value;
5096
5239
  const timestamp = yield* DateTime.now;
5097
5240
  return yield* events.publish(PromptAdmitted, {
5098
5241
  messageId: input.id,
@@ -5107,10 +5250,7 @@ const make$8 = Effect.gen(function* () {
5107
5250
  prompt: input.prompt,
5108
5251
  delivery: input.delivery,
5109
5252
  timeCreated: timestamp
5110
- }))), Effect.catchDefect((defect) => find(input.id).pipe(Effect.flatMap(Option.match({
5111
- onNone: () => Effect.die(defect),
5112
- onSome: (stored) => Effect.succeed(stored)
5113
- })))));
5253
+ }))), Effect.catchDefect((defect) => find(input.id).pipe(Effect.flatMap((stored) => Option.isSome(stored) && stored.value.cancelledSeq === void 0 ? Effect.succeed(stored.value) : Effect.die(defect)))));
5114
5254
  });
5115
5255
  const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(function* (input) {
5116
5256
  if ((yield* selectEntry(input.id)).length > 0) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
@@ -5121,7 +5261,16 @@ const make$8 = Effect.gen(function* () {
5121
5261
  delivery: input.delivery,
5122
5262
  admittedSeq: input.admittedSeq,
5123
5263
  promotedSeq: Option.none(),
5264
+ cancelledSeq: Option.none(),
5124
5265
  createdAt: VariantSchema.Override(input.timeCreated)
5266
+ })).length > 0) return;
5267
+ if ((yield* reviveRow({
5268
+ id: input.id,
5269
+ sessionId: input.sessionId,
5270
+ prompt: input.prompt,
5271
+ delivery: input.delivery,
5272
+ admittedSeq: input.admittedSeq,
5273
+ createdAt: input.timeCreated
5125
5274
  })).length === 0) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5126
5275
  }, Effect.orDie);
5127
5276
  const projectPrompted = Effect.fn("SessionInput.projectPrompted")(function* (input) {
@@ -5136,7 +5285,7 @@ const make$8 = Effect.gen(function* () {
5136
5285
  }
5137
5286
  const stored = yield* find(input.id);
5138
5287
  if (Option.isSome(stored)) {
5139
- if (!matchesProjection(stored.value, input) || stored.value.promotedSeq !== input.promotedSeq) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5288
+ if (!matchesProjection(stored.value, input) || stored.value.promotedSeq !== input.promotedSeq || stored.value.cancelledSeq !== void 0) return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5140
5289
  return;
5141
5290
  }
5142
5291
  yield* insertRow({
@@ -5146,9 +5295,50 @@ const make$8 = Effect.gen(function* () {
5146
5295
  delivery: input.delivery,
5147
5296
  admittedSeq: input.promotedSeq,
5148
5297
  promotedSeq: Option.some(input.promotedSeq),
5298
+ cancelledSeq: Option.none(),
5149
5299
  createdAt: VariantSchema.Override(input.timeCreated)
5150
5300
  });
5151
5301
  }, Effect.orDie);
5302
+ const projectCancelled = Effect.fn("SessionInput.projectCancelled")(function* (input) {
5303
+ if ((yield* cancelRow(input)).length > 0) return;
5304
+ const stored = yield* find(input.id);
5305
+ if (Option.isSome(stored) && stored.value.cancelledSeq === input.cancelledSeq) return;
5306
+ return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5307
+ }, Effect.orDie);
5308
+ const projectUpdated = Effect.fn("SessionInput.projectUpdated")(function* (input) {
5309
+ if ((yield* updateRow({
5310
+ id: input.id,
5311
+ sessionId: input.sessionId,
5312
+ prompt: input.prompt ?? null,
5313
+ delivery: input.delivery ?? null
5314
+ })).length > 0) return;
5315
+ return yield* Effect.die(new LifecycleConflict({ id: input.id }));
5316
+ }, Effect.orDie);
5317
+ const update = Effect.fn("SessionInput.update")(function* (sessionId, id, patch) {
5318
+ const stored = yield* find(id);
5319
+ if (Option.isNone(stored) || stored.value.sessionId !== sessionId || stored.value.promotedSeq !== void 0 || stored.value.cancelledSeq !== void 0) return Option.none();
5320
+ const prompt = patch.prompt === void 0 || Prompt.equivalence(patch.prompt, stored.value.prompt) ? void 0 : patch.prompt;
5321
+ const delivery = patch.delivery === void 0 || patch.delivery === stored.value.delivery ? void 0 : patch.delivery;
5322
+ if (prompt === void 0 && delivery === void 0) return stored;
5323
+ const timestamp = yield* DateTime.now;
5324
+ return yield* events.publish(PromptUpdated, {
5325
+ sessionId,
5326
+ messageId: id,
5327
+ timestamp,
5328
+ ...prompt === void 0 ? {} : { prompt },
5329
+ ...delivery === void 0 ? {} : { delivery }
5330
+ }).pipe(Effect.andThen(find(id)), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? Effect.succeed(Option.none()) : Effect.die(defect)));
5331
+ });
5332
+ const cancel = Effect.fn("SessionInput.cancel")(function* (sessionId, id) {
5333
+ const stored = yield* find(id);
5334
+ if (Option.isNone(stored) || stored.value.sessionId !== sessionId || stored.value.promotedSeq !== void 0 || stored.value.cancelledSeq !== void 0) return false;
5335
+ const timestamp = yield* DateTime.now;
5336
+ return yield* events.publish(PromptCancelled, {
5337
+ sessionId,
5338
+ messageId: id,
5339
+ timestamp
5340
+ }).pipe(Effect.as(true), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? Effect.succeed(false) : Effect.die(defect)));
5341
+ });
5152
5342
  const publishPrompted = Effect.fn("SessionInput.publishPrompted")(function* (sessionId, rows) {
5153
5343
  let promoted = 0;
5154
5344
  for (const row of rows) {
@@ -5159,7 +5349,7 @@ const make$8 = Effect.gen(function* () {
5159
5349
  messageId: id,
5160
5350
  prompt: row.prompt,
5161
5351
  delivery: row.delivery
5162
- }).pipe(Effect.as(true), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? find(id).pipe(Effect.flatMap((stored) => Option.isSome(stored) && stored.value.promotedSeq !== void 0 ? Effect.succeed(false) : Effect.die(defect))) : Effect.die(defect)))) promoted += 1;
5352
+ }).pipe(Effect.as(true), Effect.catchDefect((defect) => isLifecycleConflict(defect) ? find(id).pipe(Effect.flatMap((stored) => Option.isSome(stored) && (stored.value.promotedSeq !== void 0 || stored.value.cancelledSeq !== void 0 || stored.value.admittedSeq !== row.admittedSeq || stored.value.delivery !== row.delivery || !Prompt.equivalence(stored.value.prompt, row.prompt)) ? Effect.succeed(false) : Effect.die(defect))) : Effect.die(defect)))) promoted += 1;
5163
5353
  }
5164
5354
  return promoted;
5165
5355
  });
@@ -5168,6 +5358,13 @@ const make$8 = Effect.gen(function* () {
5168
5358
  admit,
5169
5359
  projectAdmitted,
5170
5360
  projectPrompted,
5361
+ projectCancelled,
5362
+ projectUpdated,
5363
+ update,
5364
+ cancel,
5365
+ pending: Effect.fn("SessionInput.pending")(function* (sessionId) {
5366
+ return (yield* selectPending(sessionId)).map(fromRow);
5367
+ }, Effect.orDie),
5171
5368
  hasPending: Effect.fn("SessionInput.hasPending")(function* (sessionId, delivery) {
5172
5369
  return (yield* selectPendingOne({
5173
5370
  sessionId,
@@ -5193,6 +5390,7 @@ const make$8 = Effect.gen(function* () {
5193
5390
  var control_exports = /* @__PURE__ */ __exportAll({
5194
5391
  Control: () => control_exports,
5195
5392
  PromptConflictError: () => PromptConflictError,
5393
+ RESTART_LIMIT: () => 5,
5196
5394
  Service: () => Service$5,
5197
5395
  layer: () => layer$10
5198
5396
  });
@@ -5211,6 +5409,7 @@ const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5211
5409
  const inputs = yield* make$8;
5212
5410
  const execution = yield* Service$6;
5213
5411
  const isLifecycleConflict = Schema.is(LifecycleConflict);
5412
+ const wake = (sessionId) => sessions.markRunning(sessionId).pipe(Effect.andThen(execution.wake(sessionId)));
5214
5413
  /**
5215
5414
  * Uninterruptible end to end: a client that disconnects mid-call must not
5216
5415
  * leave a prompt half-admitted. Everything durable happens inside `admit`,
@@ -5241,10 +5440,10 @@ const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5241
5440
  });
5242
5441
  return admitted;
5243
5442
  });
5244
- const prompt = Effect.fn("Control.prompt")((input) => Effect.uninterruptible(admit(input).pipe(Effect.tap((admitted) => execution.wake(admitted.sessionId)))));
5443
+ const prompt = Effect.fn("Control.prompt")((input) => Effect.uninterruptible(admit(input).pipe(Effect.tap((admitted) => wake(admitted.sessionId)))));
5245
5444
  const run = Effect.fn("Control.run")((input) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
5246
5445
  const admitted = yield* admit(input);
5247
- yield* execution.wake(admitted.sessionId);
5446
+ yield* wake(admitted.sessionId);
5248
5447
  yield* restore(execution.awaitIdle(admitted.sessionId));
5249
5448
  return admitted;
5250
5449
  })));
@@ -5256,16 +5455,40 @@ const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5256
5455
  const resume = Effect.fn("Control.resume")(function* (sessionId) {
5257
5456
  const session = yield* sessions.get(sessionId);
5258
5457
  if (Option.isNone(session)) return yield* new SessionNotFoundError({ sessionId });
5458
+ yield* sessions.markRunning(sessionId);
5259
5459
  yield* execution.resume(sessionId);
5260
5460
  });
5261
5461
  const interrupt = Effect.fn("Control.interrupt")((sessionId, options) => Effect.uninterruptible(execution.interrupt(sessionId, options?.reason ?? "user", options)));
5462
+ const update = Effect.fn("Control.update")((sessionId, id, patch) => Effect.uninterruptible(Effect.gen(function* () {
5463
+ const updated = yield* inputs.update(sessionId, id, patch);
5464
+ if (Option.isNone(updated)) return false;
5465
+ if (patch.delivery === "steer") yield* wake(sessionId);
5466
+ return true;
5467
+ })));
5468
+ const resumeUnfinished = Effect.gen(function* () {
5469
+ const running = yield* execution.active;
5470
+ const unfinished = (yield* sessions.unfinished).filter((row) => !running.has(row.id));
5471
+ yield* Effect.forEach(unfinished, Effect.fnUntraced(function* (row) {
5472
+ if ((yield* execution.active).has(row.id)) return;
5473
+ const restarts = yield* sessions.claimRestart(row.id);
5474
+ if (Option.isNone(restarts) || restarts.value >= 5) return;
5475
+ yield* execution.recover(row.id).pipe(Effect.tapCause((cause) => Effect.logWarning("Resuming an unfinished session failed", cause).pipe(Effect.annotateLogs({ sessionId: row.id }))), Effect.ignore);
5476
+ }), {
5477
+ concurrency: "unbounded",
5478
+ discard: true
5479
+ });
5480
+ }).pipe(Effect.withSpan("Control.resumeUnfinished"));
5262
5481
  return Service$5.of({
5263
5482
  prompt,
5264
5483
  run,
5265
5484
  wait,
5266
5485
  resume,
5267
5486
  interrupt,
5268
- active: execution.active
5487
+ active: execution.active,
5488
+ resumeUnfinished,
5489
+ pending: inputs.pending,
5490
+ cancel: inputs.cancel,
5491
+ update
5269
5492
  });
5270
5493
  }));
5271
5494
  //#endregion
@@ -7207,7 +7430,7 @@ const editDef = tool_exports.define({
7207
7430
  })
7208
7431
  });
7209
7432
  const fail$4 = (message) => Effect.fail(new EditFailed({ message }));
7210
- /** Filesystem failures name the backend's error code, as Pi's do. */
7433
+ /** Filesystem failures name the backend's error code. */
7211
7434
  const accessFailure = (path) => (error) => {
7212
7435
  const code = error.cause?.code;
7213
7436
  return fail$4(`Could not edit file: ${path}. Error code: ${typeof code === "string" ? code : "unknown"}.`);
@@ -9704,12 +9927,18 @@ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
9704
9927
  const runner = yield* Service$7;
9705
9928
  const events = yield* Service$16;
9706
9929
  const reportLifecycle = (sessionId, publish) => publish.pipe(Effect.tapCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logError("Failed to publish session execution lifecycle", cause).pipe(Effect.annotateLogs({ sessionId }))), Effect.ignore);
9930
+ const recovering = /* @__PURE__ */ new Set();
9931
+ const stoppedAt = /* @__PURE__ */ new Map();
9707
9932
  const coordinator = yield* make$7({
9708
- started: (sessionId) => reportLifecycle(sessionId, DateTime.now.pipe(Effect.andThen((timestamp) => events.publish(ExecutionStarted, {
9933
+ started: (sessionId) => reportLifecycle(sessionId, (recovering.has(sessionId) ? Effect.void : store.markRunning(sessionId)).pipe(Effect.andThen(DateTime.now), Effect.andThen((timestamp) => events.publish(ExecutionStarted, {
9709
9934
  sessionId,
9710
9935
  timestamp
9711
9936
  })))),
9712
9937
  settled: (sessionId, exit, reason) => reportLifecycle(sessionId, Effect.gen(function* () {
9938
+ const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
9939
+ const stop = stoppedAt.get(sessionId);
9940
+ stoppedAt.delete(sessionId);
9941
+ yield* store.endExecution(sessionId, !interrupted ? "finished" : reason === "user" ? "stopped" : "closed", stop);
9713
9942
  const timestamp = yield* DateTime.now;
9714
9943
  if (Exit.isSuccess(exit)) return yield* events.publish(ExecutionSucceeded, {
9715
9944
  sessionId,
@@ -9729,6 +9958,8 @@ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
9729
9958
  drain: Effect.fnUntraced(function* (sessionId, force) {
9730
9959
  const session = yield* store.get(sessionId);
9731
9960
  if (Option.isNone(session)) return yield* new SessionNotFoundError({ sessionId });
9961
+ const recovered = recovering.delete(sessionId);
9962
+ if (recovered && Option.isNone(session.value.execution)) return;
9732
9963
  const space = yield* store.space(sessionId);
9733
9964
  if (Option.isNone(space)) return yield* new SessionLinkedSpaceNotFoundError({ sessionId });
9734
9965
  const instanceId = space.value.env;
@@ -9749,15 +9980,30 @@ const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
9749
9980
  })));
9750
9981
  return yield* runner.run({
9751
9982
  sessionId,
9752
- force
9983
+ force,
9984
+ recovered
9753
9985
  }).pipe(Effect.provideContext(Context.merge(locationContext, mountContext)));
9754
9986
  }).pipe(Effect.scoped, Effect.tapCause((cause) => Cause.hasDies(cause) ? Effect.logError("defect", cause).pipe(Effect.annotateLogs({ sessionId })) : Effect.void));
9755
9987
  })
9756
9988
  });
9989
+ const interrupt = (sessionId, reason, options) => Effect.gen(function* () {
9990
+ const recorded = reason === "user" && !stoppedAt.has(sessionId);
9991
+ if (recorded) stoppedAt.set(sessionId, yield* events.latestSequence(sessionId));
9992
+ const accepted = yield* coordinator.interrupt(sessionId, reason, options);
9993
+ if (recorded && !accepted) stoppedAt.delete(sessionId);
9994
+ return accepted;
9995
+ });
9996
+ const recover = (sessionId) => Effect.suspend(() => {
9997
+ recovering.add(sessionId);
9998
+ return coordinator.run(sessionId);
9999
+ }).pipe(Effect.ensuring(Effect.sync(() => {
10000
+ recovering.delete(sessionId);
10001
+ })));
9757
10002
  return Service$6.of({
9758
10003
  active: coordinator.active,
9759
- interrupt: coordinator.interrupt,
10004
+ interrupt,
9760
10005
  resume: coordinator.run,
10006
+ recover,
9761
10007
  wake: coordinator.wake,
9762
10008
  awaitIdle: coordinator.awaitIdle
9763
10009
  });
@@ -11283,12 +11529,13 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
11283
11529
  }));
11284
11530
  return interruptedCause;
11285
11531
  });
11286
- const runTurnAttempt = Effect.fn("Loop.runTurnAttempt", { level: "Info" })(function* (promotion, snapshot) {
11532
+ const runTurnAttempt = Effect.fn("Loop.runTurnAttempt", { level: "Info" })(function* (promotion, snapshot, continuing) {
11287
11533
  const sessionId = snapshot.sessionId;
11288
11534
  if (promotion !== void 0) {
11289
11535
  const cutoff = yield* events.latestSequence(sessionId);
11290
- if (promotion === "followUp") yield* inputs.promoteFollowUp(sessionId);
11291
- yield* inputs.promoteSteers(sessionId, cutoff);
11536
+ const followUp = promotion === "followUp" && (yield* inputs.promoteFollowUp(sessionId));
11537
+ const steers = yield* inputs.promoteSteers(sessionId, cutoff);
11538
+ if (!continuing && !followUp && steers === 0) return { needsContinuation: false };
11292
11539
  }
11293
11540
  const assembled = yield* context.assemble(sessionId);
11294
11541
  const leaf = assembled.lastAssistant?.entryId === assembled.leafEntryId ? assembled.lastAssistant.message : void 0;
@@ -11387,8 +11634,9 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
11387
11634
  });
11388
11635
  }));
11389
11636
  });
11390
- const runTurn = Effect.fn("Loop.runTurn", { level: "Info" })(function* (promotion, snapshot) {
11391
- return yield* runTurnAttempt(promotion, snapshot);
11637
+ /** `continuing`: earlier work (tool results, a forced resume) needs a reply even when nothing is promoted. */
11638
+ const runTurn = Effect.fn("Loop.runTurn", { level: "Info" })(function* (promotion, snapshot, continuing) {
11639
+ return yield* runTurnAttempt(promotion, snapshot, continuing);
11392
11640
  });
11393
11641
  const run = Effect.fn("Loop.run", { level: "Info" })(function* (input) {
11394
11642
  yield* healDanglingDraft(input.sessionId);
@@ -11400,17 +11648,24 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
11400
11648
  "gen_ai.agent.name": "codework",
11401
11649
  "gen_ai.conversation.id": input.sessionId
11402
11650
  });
11403
- let promotion = hasSteer ? "steer" : hasFollowUp ? "followUp" : void 0;
11651
+ const cutOff = input.recovered === true && !hasSteer && hasFollowUp && (yield* context.assemble(input.sessionId).pipe(Effect.map((assembled) => {
11652
+ const leaf = assembled.lastAssistant?.entryId === assembled.leafEntryId ? assembled.lastAssistant.message : void 0;
11653
+ return assembled.messages.length > 0 && leaf?.stopReason !== "stop" && leaf?.stopReason !== "length";
11654
+ })));
11655
+ let promotion = hasSteer ? "steer" : hasFollowUp && !cutOff ? "followUp" : void 0;
11404
11656
  const drain = Effect.gen(function* () {
11405
11657
  let shouldRun = true;
11658
+ let continuing = input.force;
11406
11659
  while (shouldRun) {
11407
11660
  const snapshot = yield* state.snapshot(input.sessionId);
11408
11661
  let needsContinuation = true;
11409
11662
  while (needsContinuation) {
11410
- const result = yield* runTurn(promotion, snapshot);
11663
+ const result = yield* runTurn(promotion, snapshot, continuing);
11411
11664
  promotion = "steer";
11665
+ continuing = result.needsContinuation;
11412
11666
  needsContinuation = result.needsContinuation || (yield* inputs.hasPending(input.sessionId, "steer"));
11413
11667
  }
11668
+ continuing = false;
11414
11669
  shouldRun = yield* inputs.hasPending(input.sessionId, "followUp");
11415
11670
  promotion = shouldRun ? "followUp" : void 0;
11416
11671
  }
@@ -11532,6 +11787,20 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
11532
11787
  timeCreated: event.data.timestamp
11533
11788
  });
11534
11789
  }));
11790
+ yield* events.project(PromptCancelled, (event) => Effect.gen(function* () {
11791
+ if (event.durable === void 0) return yield* Effect.die("PromptCancelled is missing its aggregate sequence");
11792
+ yield* input.projectCancelled({
11793
+ cancelledSeq: event.durable.seq,
11794
+ id: event.data.messageId,
11795
+ sessionId: event.data.sessionId
11796
+ });
11797
+ }));
11798
+ yield* events.project(PromptUpdated, (event) => input.projectUpdated({
11799
+ id: event.data.messageId,
11800
+ sessionId: event.data.sessionId,
11801
+ ...event.data.prompt === void 0 ? {} : { prompt: event.data.prompt },
11802
+ ...event.data.delivery === void 0 ? {} : { delivery: event.data.delivery }
11803
+ }));
11535
11804
  yield* events.project(Prompted, (event) => Effect.gen(function* () {
11536
11805
  if (event.durable === void 0) return yield* Effect.die("Prompted is missing its aggregate sequence");
11537
11806
  yield* input.projectPrompted({
@@ -11617,7 +11886,7 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
11617
11886
  id: event.data.messageId,
11618
11887
  sessionId: event.data.sessionId,
11619
11888
  state: "committed"
11620
- }));
11889
+ }).pipe(Effect.andThen(sessions.resetRestarts(event.data.sessionId))));
11621
11890
  }));
11622
11891
  //#endregion
11623
11892
  //#region src/session/live.ts
@@ -11896,6 +12165,7 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
11896
12165
  const config = Option.getOrUndefined(row.config);
11897
12166
  const model = config?.model;
11898
12167
  const thinkingLevel = config?.thinkingLevel;
12168
+ const parked = Option.isSome(row.execution) && row.restarts >= 5 && !(yield* control.active).has(id);
11899
12169
  return {
11900
12170
  id,
11901
12171
  title: row.title,
@@ -11909,7 +12179,8 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
11909
12179
  } },
11910
12180
  updatedAt: DateTime.toEpochMillis(row.updatedAt),
11911
12181
  ...model === void 0 ? {} : { model },
11912
- ...thinkingLevel === void 0 ? {} : { thinkingLevel }
12182
+ ...thinkingLevel === void 0 ? {} : { thinkingLevel },
12183
+ parked
11913
12184
  };
11914
12185
  }).pipe(Effect.withSpan("Session.info"));
11915
12186
  return {