@bridge4dev/runner 0.27.0 → 0.29.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.
package/dist/config.d.ts CHANGED
@@ -64,6 +64,25 @@ declare const ConfigSchema: z.ZodObject<{
64
64
  }, {
65
65
  enabled?: boolean | undefined;
66
66
  }>>;
67
+ /**
68
+ * Ticket #126: the machine owner's veto over restore points.
69
+ *
70
+ * A restore point is a copy of the working tree, kept on this machine, in an
71
+ * object store of the runner's own. That is disk this server's owner pays
72
+ * for and content they may not want duplicated at all — so the same rule as
73
+ * `[verify]` applies: `enabled = false` means the capability is not
74
+ * announced, the dashboard draws no rewind action, and no snapshot is ever
75
+ * taken. Secrets are excluded from a checkpoint in any case (`policy.ts`
76
+ * decides what counts), but "excluded" is a promise, and switching the whole
77
+ * thing off is a fact.
78
+ */
79
+ checkpoints: z.ZodOptional<z.ZodObject<{
80
+ enabled: z.ZodDefault<z.ZodBoolean>;
81
+ }, "strip", z.ZodTypeAny, {
82
+ enabled: boolean;
83
+ }, {
84
+ enabled?: boolean | undefined;
85
+ }>>;
67
86
  }, "strip", z.ZodTypeAny, {
68
87
  api: {
69
88
  url: string;
@@ -74,6 +93,9 @@ declare const ConfigSchema: z.ZodObject<{
74
93
  name: string;
75
94
  token: string;
76
95
  };
96
+ checkpoints?: {
97
+ enabled: boolean;
98
+ } | undefined;
77
99
  mcp?: {
78
100
  url: string;
79
101
  token: string;
@@ -97,6 +119,9 @@ declare const ConfigSchema: z.ZodObject<{
97
119
  name: string;
98
120
  token: string;
99
121
  };
122
+ checkpoints?: {
123
+ enabled?: boolean | undefined;
124
+ } | undefined;
100
125
  mcp?: {
101
126
  url: string;
102
127
  token: string;
package/dist/config.js CHANGED
@@ -59,6 +59,23 @@ const ConfigSchema = z.object({
59
59
  enabled: z.boolean().default(true),
60
60
  })
61
61
  .optional(),
62
+ /**
63
+ * Ticket #126: the machine owner's veto over restore points.
64
+ *
65
+ * A restore point is a copy of the working tree, kept on this machine, in an
66
+ * object store of the runner's own. That is disk this server's owner pays
67
+ * for and content they may not want duplicated at all — so the same rule as
68
+ * `[verify]` applies: `enabled = false` means the capability is not
69
+ * announced, the dashboard draws no rewind action, and no snapshot is ever
70
+ * taken. Secrets are excluded from a checkpoint in any case (`policy.ts`
71
+ * decides what counts), but "excluded" is a promise, and switching the whole
72
+ * thing off is a fact.
73
+ */
74
+ checkpoints: z
75
+ .object({
76
+ enabled: z.boolean().default(true),
77
+ })
78
+ .optional(),
62
79
  });
63
80
  export function loadConfig() {
64
81
  const file = configFilePath();
package/dist/index.js CHANGED
@@ -116,6 +116,8 @@ function runnerCapabilities(apiUrlOverride) {
116
116
  // feature is that a DevBridge manager approves every command first — but the
117
117
  // person who owns the server gets the last word on whether it exists here.
118
118
  const verifyEnabled = config?.verify?.enabled !== false;
119
+ // Ticket #126: the same veto, for restore points. Default on.
120
+ const checkpointsEnabled = config?.checkpoints?.enabled !== false;
119
121
  return {
120
122
  agents: installedAgents(),
121
123
  git: true,
@@ -231,6 +233,22 @@ function runnerCapabilities(apiUrlOverride) {
231
233
  ...(verifyEnabled ? { previewWorktrees: true } : {}),
232
234
  /** Session 14: one-shot agent-written commit messages. */
233
235
  commitMessages: true,
236
+ /**
237
+ * Ticket #126: restore points for the working tree, and the rewind that
238
+ * uses them. Withheld — not reported as false — when the machine's owner
239
+ * switched them off, by the same rule as `verifyRuns`.
240
+ */
241
+ ...(checkpointsEnabled
242
+ ? { sessionCheckpoints: true }
243
+ : { checkpointsBlocked: 'disabled-by-config' }),
244
+ /**
245
+ * Ticket #126: rewinding what the AGENT remembers, and `/compact` on
246
+ * demand. Both agents can do both — Claude through `resumeSessionAt` +
247
+ * `forkSession`, Codex through the stable `thread/fork { lastTurnId }` and
248
+ * `thread/compact/start` of app-server 0.145.0.
249
+ */
250
+ contextRewind: true,
251
+ contextCompaction: true,
234
252
  /**
235
253
  * Session 15: `git_status` reports whether the PROJECT FOLDER is clean, so
236
254
  * the panel can say what is blocking an Apply instead of offering a button
@@ -321,6 +339,17 @@ function runnerCapabilities(apiUrlOverride) {
321
339
  'git_merge_abort',
322
340
  'recipe_state',
323
341
  'propose_commit_message',
342
+ 'recall_message',
343
+ 'compact_context',
344
+ ...(checkpointsEnabled
345
+ ? [
346
+ 'session_checkpoint',
347
+ 'session_checkpoints',
348
+ 'session_rewind_preview',
349
+ 'session_rewind_files',
350
+ 'context_rewind',
351
+ ]
352
+ : []),
324
353
  ...(verifyEnabled
325
354
  ? ['verify_start', 'verify_status', 'verify_cancel', 'preview_checkout', 'preview_stop']
326
355
  : []),
@@ -485,6 +514,7 @@ async function cmdDaemon() {
485
514
  // The same veto the capability list honours — announced AND enforced, so a
486
515
  // frame from an API that has not noticed still cannot start a run.
487
516
  verifyEnabled: config.verify?.enabled !== false,
517
+ checkpointsEnabled: config.checkpoints?.enabled !== false,
488
518
  apiUrl: config.api.url,
489
519
  // Used to fetch the files a user attaches to a message (session 10) — the
490
520
  // same token the WS connection authenticates with, never passed onwards.
package/dist/journal.d.ts CHANGED
@@ -20,6 +20,16 @@ export interface PendingMessage {
20
20
  * session worktree is guaranteed to exist.
21
21
  */
22
22
  attachments?: PendingAttachment[];
23
+ /**
24
+ * Feed seq of the `message` event this text was echoed under (ticket #125).
25
+ *
26
+ * The only identifier the queue and the browser share: the API never sees
27
+ * `id` (it is minted here), and the `session_message` frame carries no id of
28
+ * its own — so the seq of the bubble the user is looking at is what a recall
29
+ * has to name. Absent on records written by a runner older than 0.28.0:
30
+ * those cannot be recalled, which is correct — they predate the feature.
31
+ */
32
+ seq?: number;
23
33
  }
24
34
  export interface PendingAttachment {
25
35
  id: string;
@@ -43,6 +53,11 @@ export declare class SessionJournal {
43
53
  extra?: Record<string, unknown>;
44
54
  epoch?: number;
45
55
  } | null;
56
+ /** The agent's conversation tip, and the provider session it lives in. */
57
+ lastAnchor: {
58
+ anchor: string;
59
+ providerSessionId: string;
60
+ } | null;
46
61
  constructor(sessionId: string, dir?: string);
47
62
  private replay;
48
63
  private write;
@@ -60,6 +75,14 @@ export declare class SessionJournal {
60
75
  * Statuses are fire-and-forget on the wire; journaling the latest one lets
61
76
  * the supervisor re-report it after a reconnect (QA-96 F1).
62
77
  */
78
+ /**
79
+ * Remember the conversation tip across a process gap (ticket #126).
80
+ *
81
+ * Written only when it actually moves: an agent that says nothing writes
82
+ * nothing, and the same line repeated every turn would grow the file for no
83
+ * reason.
84
+ */
85
+ recordAnchor(anchor: string, providerSessionId: string): void;
63
86
  recordStatus(status: string, extra?: Record<string, unknown>, epoch?: number): void;
64
87
  /** Assign the next seq and persist the event before it is sent. */
65
88
  append(eventType: string, payload: Record<string, unknown>): JournalEvent;
@@ -77,9 +100,19 @@ export declare class SessionJournal {
77
100
  * it is queued in memory, so the ordering is "on disk, then held" — a crash
78
101
  * between the two costs a duplicate delivery at worst, never a lost message.
79
102
  */
80
- appendPending(text: string, attachments?: PendingAttachment[]): PendingMessage;
103
+ appendPending(text: string, attachments?: PendingAttachment[], seq?: number): PendingMessage;
81
104
  /** The message reached an agent — stop replaying it after a restart. */
82
105
  resolvePending(id: string): void;
106
+ /**
107
+ * The user recalled the message before any agent saw it (ticket #125).
108
+ *
109
+ * Returns false when the record is already gone — which is the answer to
110
+ * "did I win the race", and the only safe way to ask it: the caller must
111
+ * report `already_delivered` rather than a success it cannot back up.
112
+ */
113
+ cancelPending(id: string): boolean;
114
+ /** The queued record echoed under this feed seq, if it is still queued. */
115
+ findPendingBySeq(seq: number): PendingMessage | undefined;
83
116
  /** Messages still waiting, oldest first (ids are minted in order). */
84
117
  pending(): PendingMessage[];
85
118
  get lastAssignedSeq(): number;
package/dist/journal.js CHANGED
@@ -19,6 +19,8 @@ export class SessionJournal {
19
19
  bytesOnDisk = 0;
20
20
  /** Last status reported for this session — replayed after a reconnect. */
21
21
  lastStatus = null;
22
+ /** The agent's conversation tip, and the provider session it lives in. */
23
+ lastAnchor = null;
22
24
  constructor(sessionId, dir = journalDir()) {
23
25
  this.sessionId = sessionId;
24
26
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -52,6 +54,9 @@ export class SessionJournal {
52
54
  else if (parsed.kind === 'ack') {
53
55
  this.unackedBySeq.delete(parsed.seq);
54
56
  }
57
+ else if (parsed.kind === 'anchor') {
58
+ this.lastAnchor = { anchor: parsed.anchor, providerSessionId: parsed.providerSessionId };
59
+ }
55
60
  else if (parsed.kind === 'status') {
56
61
  this.lastStatus = {
57
62
  status: parsed.status,
@@ -64,6 +69,7 @@ export class SessionJournal {
64
69
  id: parsed.id,
65
70
  text: parsed.text,
66
71
  ...(parsed.attachments?.length ? { attachments: parsed.attachments } : {}),
72
+ ...(typeof parsed.seq === 'number' ? { seq: parsed.seq } : {}),
67
73
  });
68
74
  // Ids are `p<n>`; keep the counter above whatever the file holds so a
69
75
  // restarted runner cannot mint an id that is already in flight.
@@ -71,7 +77,7 @@ export class SessionJournal {
71
77
  if (Number.isFinite(n) && n >= this.pendingCounter)
72
78
  this.pendingCounter = n + 1;
73
79
  }
74
- else if (parsed.kind === 'pending_done') {
80
+ else if (parsed.kind === 'pending_done' || parsed.kind === 'pending_cancelled') {
75
81
  this.pendingById.delete(parsed.id);
76
82
  }
77
83
  else if (parsed.kind === 'seq') {
@@ -98,6 +104,9 @@ export class SessionJournal {
98
104
  for (const event of this.unacked()) {
99
105
  lines.push({ kind: 'event', ...event, ts: new Date().toISOString() });
100
106
  }
107
+ if (this.lastAnchor) {
108
+ lines.push({ kind: 'anchor', ...this.lastAnchor });
109
+ }
101
110
  if (this.lastStatus) {
102
111
  lines.push({
103
112
  kind: 'status',
@@ -115,6 +124,7 @@ export class SessionJournal {
115
124
  id: record.id,
116
125
  text: record.text,
117
126
  ...(record.attachments?.length ? { attachments: record.attachments } : {}),
127
+ ...(record.seq === undefined ? {} : { seq: record.seq }),
118
128
  ts: new Date().toISOString(),
119
129
  });
120
130
  }
@@ -138,6 +148,21 @@ export class SessionJournal {
138
148
  * Statuses are fire-and-forget on the wire; journaling the latest one lets
139
149
  * the supervisor re-report it after a reconnect (QA-96 F1).
140
150
  */
151
+ /**
152
+ * Remember the conversation tip across a process gap (ticket #126).
153
+ *
154
+ * Written only when it actually moves: an agent that says nothing writes
155
+ * nothing, and the same line repeated every turn would grow the file for no
156
+ * reason.
157
+ */
158
+ recordAnchor(anchor, providerSessionId) {
159
+ if (this.lastAnchor?.anchor === anchor &&
160
+ this.lastAnchor.providerSessionId === providerSessionId) {
161
+ return;
162
+ }
163
+ this.lastAnchor = { anchor, providerSessionId };
164
+ this.write({ kind: 'anchor', anchor, providerSessionId });
165
+ }
141
166
  recordStatus(status, extra, epoch) {
142
167
  this.lastStatus = {
143
168
  status,
@@ -187,18 +212,20 @@ export class SessionJournal {
187
212
  * it is queued in memory, so the ordering is "on disk, then held" — a crash
188
213
  * between the two costs a duplicate delivery at worst, never a lost message.
189
214
  */
190
- appendPending(text, attachments) {
215
+ appendPending(text, attachments, seq) {
191
216
  const id = `p${this.pendingCounter++}`;
192
217
  const record = {
193
218
  id,
194
219
  text,
195
220
  ...(attachments?.length ? { attachments } : {}),
221
+ ...(seq === undefined ? {} : { seq }),
196
222
  };
197
223
  this.write({
198
224
  kind: 'pending',
199
225
  id,
200
226
  text,
201
227
  ...(attachments?.length ? { attachments } : {}),
228
+ ...(seq === undefined ? {} : { seq }),
202
229
  ts: new Date().toISOString(),
203
230
  });
204
231
  this.pendingById.set(id, record);
@@ -211,6 +238,28 @@ export class SessionJournal {
211
238
  this.pendingById.delete(id);
212
239
  this.write({ kind: 'pending_done', id });
213
240
  }
241
+ /**
242
+ * The user recalled the message before any agent saw it (ticket #125).
243
+ *
244
+ * Returns false when the record is already gone — which is the answer to
245
+ * "did I win the race", and the only safe way to ask it: the caller must
246
+ * report `already_delivered` rather than a success it cannot back up.
247
+ */
248
+ cancelPending(id) {
249
+ if (!this.pendingById.has(id))
250
+ return false;
251
+ this.pendingById.delete(id);
252
+ this.write({ kind: 'pending_cancelled', id });
253
+ return true;
254
+ }
255
+ /** The queued record echoed under this feed seq, if it is still queued. */
256
+ findPendingBySeq(seq) {
257
+ for (const record of this.pendingById.values()) {
258
+ if (record.seq === seq)
259
+ return record;
260
+ }
261
+ return undefined;
262
+ }
214
263
  /** Messages still waiting, oldest first (ids are minted in order). */
215
264
  pending() {
216
265
  return [...this.pendingById.values()];
package/dist/paths.d.ts CHANGED
@@ -22,6 +22,16 @@ export declare function worktreesDir(): string;
22
22
  export declare function knownWorkspacesPath(): string;
23
23
  /** Session 14: the single preview checkout per repository. */
24
24
  export declare function previewsDir(): string;
25
+ /**
26
+ * Ticket #126: git object stores holding the sessions' restore points.
27
+ *
28
+ * Deliberately under `devbridge-runner/`, and deliberately NOT under
29
+ * `worktrees/` or `previews/` — those two are excluded from the runner-tree
30
+ * rule in `SECRET_PATH_PATTERNS` (the agent has to be able to read its own
31
+ * workspace), so anything that must stay unreadable to the agent belongs
32
+ * exactly here.
33
+ */
34
+ export declare function checkpointsDir(): string;
25
35
  /**
26
36
  * Per-session MCP config files (ticket #119).
27
37
  *
package/dist/paths.js CHANGED
@@ -54,6 +54,18 @@ export function knownWorkspacesPath() {
54
54
  export function previewsDir() {
55
55
  return path.join(stateDir(), 'previews');
56
56
  }
57
+ /**
58
+ * Ticket #126: git object stores holding the sessions' restore points.
59
+ *
60
+ * Deliberately under `devbridge-runner/`, and deliberately NOT under
61
+ * `worktrees/` or `previews/` — those two are excluded from the runner-tree
62
+ * rule in `SECRET_PATH_PATTERNS` (the agent has to be able to read its own
63
+ * workspace), so anything that must stay unreadable to the agent belongs
64
+ * exactly here.
65
+ */
66
+ export function checkpointsDir() {
67
+ return path.join(stateDir(), 'checkpoints');
68
+ }
57
69
  /**
58
70
  * Per-session MCP config files (ticket #119).
59
71
  *
package/dist/policy.js CHANGED
@@ -97,6 +97,16 @@ const SECRET_PATH_PATTERNS = [
97
97
  // its own workspace — caught by the existing suite the moment it was tried.
98
98
  /(^|\/)devbridge-mcp\.[^/]*\.json$/,
99
99
  /(^|\/)devbridge-runner(\/(?!worktrees(\/|$)|previews(\/|$))|$)/,
100
+ // Ticket #126: the object store holding this session's restore points — a
101
+ // full copy of the working tree as it was at every turn.
102
+ //
103
+ // The runner-directory rule above already covers it on an ordinary install.
104
+ // This one is for the case that rule cannot see: under a
105
+ // `DEVBRIDGE_RUNNER_HOME` override the path carries no `devbridge-runner`
106
+ // segment at all, which is exactly the gap the MCP-config entry was added
107
+ // for. Matching the distinctive `checkpoints/<hash>.git` shape closes it
108
+ // wherever the state directory ends up.
109
+ /(^|\/)checkpoints\/[0-9a-f]{8,}\.git(\/|$)/,
100
110
  /(^|\/)(shadow|passwd|sudoers)$/,
101
111
  /(^|\/)\.netrc$/,
102
112
  /(^|\/)\.git-credentials$/,
@@ -440,6 +450,11 @@ const SECRET_COMMAND_PATTERNS = [
440
450
  // never contains the suffix.
441
451
  /devbridge-mcp/,
442
452
  /devbridge-runner\/(?!worktrees|previews)/,
453
+ // Ticket #126 — the command form of the restore-point entry above. Written
454
+ // as its own rule for the same reason gotcha #110 exists: a path in one
455
+ // secret table and not the other is theatre, and `git --git-dir=…` /
456
+ // `cat …/checkpoints/<hash>.git/…` walks past a Read-tool guard entirely.
457
+ /checkpoints\/[0-9a-f]{8,}\.git/,
443
458
  /devbridge-runner(?=\s|$|['"])/,
444
459
  /\/etc\/(shadow|passwd|sudoers)/,
445
460
  /\.git-credentials/,
@@ -126,8 +126,8 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
126
126
  model: string | null;
127
127
  effort: string | null;
128
128
  prompt: string;
129
- epoch: number;
130
129
  providerSessionId: string | null;
130
+ epoch: number;
131
131
  lastSeq: number;
132
132
  costUsd: number;
133
133
  activeMsBase: number;
@@ -328,8 +328,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
328
328
  model: string | null;
329
329
  effort: string | null;
330
330
  prompt: string;
331
- epoch: number;
332
331
  providerSessionId: string | null;
332
+ epoch: number;
333
333
  lastSeq: number;
334
334
  costUsd: number;
335
335
  activeMsBase: number;
@@ -410,8 +410,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
410
410
  model: string | null;
411
411
  effort: string | null;
412
412
  prompt: string;
413
- epoch: number;
414
413
  providerSessionId: string | null;
414
+ epoch: number;
415
415
  lastSeq: number;
416
416
  costUsd: number;
417
417
  activeMsBase: number;
@@ -642,8 +642,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
642
642
  model: string | null;
643
643
  effort: string | null;
644
644
  prompt: string;
645
- epoch: number;
646
645
  providerSessionId: string | null;
646
+ epoch: number;
647
647
  lastSeq: number;
648
648
  costUsd: number;
649
649
  activeMsBase: number;
@@ -722,8 +722,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
722
722
  model: string | null;
723
723
  effort: string | null;
724
724
  prompt: string;
725
- epoch: number;
726
725
  providerSessionId: string | null;
726
+ epoch: number;
727
727
  lastSeq: number;
728
728
  costUsd: number;
729
729
  activeMsBase: number;
@@ -39,6 +39,14 @@ export interface SupervisorOptions {
39
39
  verifyEnabled?: boolean;
40
40
  /** Test seam for the one-shot commit-message run. */
41
41
  proposeCommitMessage?: typeof proposeCommitMessage;
42
+ /**
43
+ * `[checkpoints] enabled` from the runner's own config (ticket #126), by the
44
+ * same rule as `[verify] enabled`: restore points are copies of the working
45
+ * tree kept on this machine, so the machine's owner has the last word on
46
+ * whether they are taken at all. `false` means the capability is not
47
+ * announced and no rewind can be started.
48
+ */
49
+ checkpointsEnabled?: boolean;
42
50
  }
43
51
  export declare class Supervisor {
44
52
  private readonly ws;
@@ -61,6 +69,17 @@ export declare class Supervisor {
61
69
  private readonly authRelay;
62
70
  /** Serialises repo-mutating git commands per workspace repo (QA-99 MAJOR-1). */
63
71
  private readonly repoLocks;
72
+ /**
73
+ * Repo keys with work in flight right now (ticket #126).
74
+ *
75
+ * A checkpoint deliberately does NOT take the repo lock — it only reads the
76
+ * worktree, and queueing a user's message behind a 150-second push would be
77
+ * a plain regression. What it does instead is decline to run while a
78
+ * mutating command holds the repo: a snapshot taken halfway through an apply
79
+ * would be a state that never existed, and offering to restore it is worse
80
+ * than having no restore point for that one message.
81
+ */
82
+ private readonly repoLockDepth;
64
83
  /** An update is installing right now — a second one would fight it. */
65
84
  private selfUpdateInFlight;
66
85
  /** Session 14: one project-recipe run per machine, and its verdict queue. */
@@ -198,6 +217,14 @@ export declare class Supervisor {
198
217
  * slot.
199
218
  */
200
219
  private isParkable;
220
+ /**
221
+ * Stop the agent process but keep the session resumable.
222
+ *
223
+ * `quiet` is for the callers that say it better themselves: a conversation
224
+ * rewind parks the session too, and announcing «the runner switched to
225
+ * another session» there is simply untrue — nothing switched, the user
226
+ * rewound (ticket #126).
227
+ */
201
228
  private park;
202
229
  private forwardEvent;
203
230
  /**
@@ -210,6 +237,16 @@ export declare class Supervisor {
210
237
  */
211
238
  private onQuestionAnswer;
212
239
  private onUserMessage;
240
+ /**
241
+ * Queue a message that no agent can take yet, and say so in the feed
242
+ * (ticket #125).
243
+ *
244
+ * Every queueing decision goes through here so the browser's idea of "this
245
+ * one can still be taken back" cannot drift from the runner's. Records with
246
+ * no seq — orphans, and anything written by a runner older than 0.28.0 —
247
+ * stay silent: they have no name the browser could quote back.
248
+ */
249
+ private queueMessage;
213
250
  /**
214
251
  * Run delivery work for one session, strictly after whatever is already
215
252
  * queued for it. Order is the whole point: two messages typed seconds apart
@@ -235,6 +272,39 @@ export declare class Supervisor {
235
272
  * the message is retired from disk only once an agent has it.
236
273
  */
237
274
  private deliverMessage;
275
+ /**
276
+ * Nothing could take the message: put it back where it came from.
277
+ *
278
+ * Records that were already held keep their identity (and their queued
279
+ * announcement); a first-time refusal mints one and announces it.
280
+ */
281
+ private requeue;
282
+ /**
283
+ * Where this session's conversation stands, live process or not (ticket #126).
284
+ *
285
+ * Reads the running agent when there is one and remembers what it said;
286
+ * falls back to that memory when there is not. The provider session id is
287
+ * checked on the way out rather than on the way in: an anchor minted before
288
+ * a fork is not wrong, it simply belongs to a conversation this session no
289
+ * longer has.
290
+ */
291
+ private currentAnchor;
292
+ /**
293
+ * Take a restore point in front of the work that is about to start
294
+ * (ticket #126).
295
+ *
296
+ * Called from the delivery chain — which is already asynchronous because of
297
+ * attachments — rather than from `deliverMessage`, which is synchronous and
298
+ * has five exits. Never throws and never blocks delivery: a message must
299
+ * reach the agent whether or not a restore point could be taken.
300
+ *
301
+ * Three reasons it declines, and each of them is a state in which a snapshot
302
+ * would be a lie rather than a restore point:
303
+ * - the machine's owner switched checkpoints off;
304
+ * - the agent is mid-turn, so the tree is being written to as we read it;
305
+ * - a repo-mutating command holds the repository.
306
+ */
307
+ private captureCheckpoint;
238
308
  /**
239
309
  * Deliver messages that raced session start (already journaled).
240
310
  *
@@ -263,15 +333,31 @@ export declare class Supervisor {
263
333
  * mark the other session failed (QA-99 MAJOR-1).
264
334
  */
265
335
  private withRepoLock;
336
+ /** Is a repo-mutating command queued or running for this path right now? */
337
+ private isRepoLocked;
266
338
  /**
267
339
  * Same lock, keyed by the shared repository rather than by whichever path the
268
340
  * caller happened to have. A worktree commit and a workspace squash-merge
269
341
  * touch one repo, so they must take one key (see `repoKeyFor`).
270
342
  */
271
343
  private withRepoLockFor;
344
+ /**
345
+ * The live session this command names, or the sentence to refuse it with.
346
+ *
347
+ * A string return is deliberately unmistakable for a session: every caller
348
+ * has to branch on the type, so "I did not check" cannot compile.
349
+ */
350
+ private requireSession;
272
351
  /** A session actively mid-turn in this worktree — git writes must wait. */
273
352
  private isWorktreeBusy;
274
353
  private static readonly EVENT_PAYLOAD_CAP;
354
+ /**
355
+ * Journal an event, put it on the wire, and return it.
356
+ *
357
+ * The return value matters since ticket #125: the seq assigned here is the
358
+ * only name the browser and the runner share for one message, so the caller
359
+ * that echoes a user bubble has to be able to read it back.
360
+ */
275
361
  private sendEvent;
276
362
  private reportStatus;
277
363
  /** Graceful daemon shutdown: kill agents, keep sessions resumable server-side. */