@bridge4dev/runner 0.34.0 → 0.35.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.
@@ -483,21 +483,41 @@ class CodexSession {
483
483
  // settings/update in this protocol version).
484
484
  ...(this.effort ? { effort: this.effort } : {}),
485
485
  };
486
- // Collaboration mode is turn-scoped but sticky, and its `settings` REPLACE
487
- // the thread's developer instructions — so only send it on a real change.
486
+ // Collaboration mode is turn-scoped but sticky, so it is only sent on a real
487
+ // change. What it must NOT carry is our own instructions (ticket #179).
488
488
  //
489
- // Session 13: they are sent for plan mode too. Passing `null` there did not
490
- // mean «keep what the thread had», it meant «no developer instructions at
491
- // all» — so the one mode a user picks when they want the agent to think
492
- // before acting was the one mode where DevBridge's rules, the branch it is
493
- // on and the project's own CLAUDE.md never reached it.
489
+ // The line that used to sit here said `settings` REPLACE the thread's
490
+ // developer instructions, and that passing `null` meant «no developer
491
+ // instructions at all». Both halves are false, and the first one cost the
492
+ // product a duplicated system prompt in every Codex session. Measured on the
493
+ // wire — a fake Responses API behind `codex app-server`, reading the developer
494
+ // message verbatim — on codex-cli 0.135.0 AND 0.147.0, which behave alike:
495
+ //
496
+ // thread developerInstructions + collaborationMode carrying the same text
497
+ // → the text appears TWICE in one developer message: once at the top (on
498
+ // 0.135.0, right after `<permissions instructions>`) and again inside
499
+ // `<collaboration_mode>…</collaboration_mode>`. That is exactly what the
500
+ // ticket's screenshots show, and with a real project prompt it is ~27 KB
501
+ // sent twice on every single request.
502
+ // thread developerInstructions + `developer_instructions: null`
503
+ // → the text appears ONCE, and `<collaboration_mode>` carries Codex's own
504
+ // preset for the mode («# Plan Mode (Conversational)…»).
505
+ //
506
+ // So `null` is not a loss, it is the fix — and it repairs a second, quieter
507
+ // defect at the same time: our text was OVERWRITING Codex's built-in plan
508
+ // instructions, i.e. the one mode a user picks to make the agent think before
509
+ // acting was the one mode that never received the instructions for thinking
510
+ // before acting. The thread-level channel (`threadParams`) stays the single
511
+ // source of DevBridge's rules, exactly like `systemPrompt.append` on Claude.
494
512
  if (this.lastCollabMode !== wantCollab) {
495
513
  params['collaborationMode'] = {
496
514
  mode: wantCollab,
497
515
  settings: {
498
516
  model: this.model ?? this.threadModel ?? 'gpt-5.5',
499
517
  reasoning_effort: null,
500
- developer_instructions: composeSystemAppend(this.spec),
518
+ // «Use the built-in instructions for the selected mode» — the app-server
519
+ // schema's own words. Ours already arrived with the thread.
520
+ developer_instructions: null,
501
521
  },
502
522
  };
503
523
  this.lastCollabMode = wantCollab;
package/dist/index.js CHANGED
@@ -35,6 +35,18 @@ const LAST_EXIT = takeLastExit();
35
35
  function print(line) {
36
36
  process.stdout.write(line + '\n');
37
37
  }
38
+ /**
39
+ * Дата для оператора, который читает вывод у себя в консоли.
40
+ *
41
+ * Раньше здесь стояло `iso.slice(0, 10)` — это UTC-день, а не день того, кто
42
+ * смотрит: к востоку от Гринвича поздним вечером он показывает вчерашнее число.
43
+ * Часы машины оператора — единственный разумный ответ: у раннера нет ни
44
+ * человека с настройкой, ни организации.
45
+ */
46
+ function formatLocalDay(iso) {
47
+ const date = new Date(iso);
48
+ return Number.isNaN(date.getTime()) ? iso.slice(0, 10) : date.toLocaleDateString();
49
+ }
38
50
  function fail(message) {
39
51
  process.stderr.write(`error: ${message}\n`);
40
52
  process.exit(1);
@@ -908,7 +920,7 @@ async function agentChecks() {
908
920
  ok: signedIn,
909
921
  name: `${agent} login`,
910
922
  detail: (info.detail ?? info.status) +
911
- (info.expiresAt ? ` · until ${info.expiresAt.slice(0, 10)}` : ''),
923
+ (info.expiresAt ? ` · until ${formatLocalDay(info.expiresAt)}` : ''),
912
924
  ...(signedIn
913
925
  ? {}
914
926
  : {
@@ -136,6 +136,20 @@ export declare class Supervisor {
136
136
  * refuse writes to it: in `workMode: DIRECT` the project folder is the
137
137
  * agent's own working directory, so without that rule a session could rewrite
138
138
  * the prompt it will itself be started with next time (QA-130 MAJOR-3).
139
+ *
140
+ * @param resuming This launch continues an existing agent conversation. It
141
+ * changes only the WORDS, never the behaviour — and the words matter,
142
+ * because a person who has just watched their session crash reads a second
143
+ * «Project prompt loaded» as «my context was thrown away» (ticket #177).
144
+ * Measured, so the line can say it plainly: a system prompt lives in the
145
+ * agent PROCESS, not in the saved conversation. Resuming Claude Code
146
+ * without re-supplying `--append-system-prompt` restores every message and
147
+ * ZERO bytes of the appended prompt; re-supplying it restores the same
148
+ * messages and exactly ONE copy of the prompt, never two. Codex is the same
149
+ * fact by a different route — it rebuilds the developer message from the
150
+ * current parameters on every request. So re-reading the file here is not
151
+ * waste, and skipping it would silently strip the project's rules (and
152
+ * DevBridge's own) from every session that ever resumed.
139
153
  */
140
154
  private resolveAgentPrompt;
141
155
  /** Warn the user when this share of the budget is gone. */
@@ -358,8 +358,19 @@ export class Supervisor {
358
358
  if (descriptor.epoch > 0) {
359
359
  // The API owns the resume transition; the feed marker has to come from
360
360
  // here because the runner is the only writer of the event seq.
361
+ //
362
+ // Ticket #177: which of the two sentences is true depends on whether
363
+ // there is a conversation to go back to. `providerSessionId` is the
364
+ // agent's own name for it, and it is what the next launch hands to
365
+ // `--resume` / `thread/resume`. Without it the next process starts the
366
+ // conversation over — which is a real loss, and promising «continue
367
+ // where the agent left off» there is the one thing the feed must not do.
368
+ // It happens for real: a process that dies before it reports its session
369
+ // id (the SIGABRT this ticket came from) leaves the row with none.
361
370
  this.sendEvent(running, 'system_note', {
362
- text: 'Session resumed — send a message to continue where the agent left off.',
371
+ text: descriptor.providerSessionId
372
+ ? 'Session resumed — the agent still has this conversation. Send a message to continue where it left off.'
373
+ : 'Session resumed, but the agent never got as far as naming its conversation, so it starts this one over. Your files, your branch and everything above are untouched.',
363
374
  });
364
375
  }
365
376
  running.lastReported = descriptor.status === 'REVIEW' ? 'REVIEW' : 'WAITING_INPUT';
@@ -444,7 +455,14 @@ export class Supervisor {
444
455
  // Facts about this session only, plus the one file the project named. The
445
456
  // rest of the project's documentation is read by each agent itself — see
446
457
  // `composeWorkspaceContext`.
447
- const agentPrompt = this.resolveAgentPrompt(running);
458
+ // Does this launch CONTINUE the agent's conversation or begin a new one?
459
+ // Read before `rewindAnchor` is consumed below, and without consuming it —
460
+ // the prompt notice says different things about the two cases (#177).
461
+ // A rewind resumes a conversation too, but a CUT one — so it gets neither
462
+ // «loaded» nor «unchanged».
463
+ const rewinding = Boolean(running.rewindAnchor?.agentSession);
464
+ const resuming = rewinding || Boolean(resumeId);
465
+ const agentPrompt = this.resolveAgentPrompt(running, resuming && !rewinding);
448
466
  const workspaceContext = composeWorkspaceContext(descriptor, agentPrompt?.text);
449
467
  const rewind = running.rewindAnchor;
450
468
  delete running.rewindAnchor;
@@ -515,8 +533,22 @@ export class Supervisor {
515
533
  * refuse writes to it: in `workMode: DIRECT` the project folder is the
516
534
  * agent's own working directory, so without that rule a session could rewrite
517
535
  * the prompt it will itself be started with next time (QA-130 MAJOR-3).
536
+ *
537
+ * @param resuming This launch continues an existing agent conversation. It
538
+ * changes only the WORDS, never the behaviour — and the words matter,
539
+ * because a person who has just watched their session crash reads a second
540
+ * «Project prompt loaded» as «my context was thrown away» (ticket #177).
541
+ * Measured, so the line can say it plainly: a system prompt lives in the
542
+ * agent PROCESS, not in the saved conversation. Resuming Claude Code
543
+ * without re-supplying `--append-system-prompt` restores every message and
544
+ * ZERO bytes of the appended prompt; re-supplying it restores the same
545
+ * messages and exactly ONE copy of the prompt, never two. Codex is the same
546
+ * fact by a different route — it rebuilds the developer message from the
547
+ * current parameters on every request. So re-reading the file here is not
548
+ * waste, and skipping it would silently strip the project's rules (and
549
+ * DevBridge's own) from every session that ever resumed.
518
550
  */
519
- resolveAgentPrompt(running) {
551
+ resolveAgentPrompt(running, resuming = false) {
520
552
  const { descriptor } = running;
521
553
  const configured = descriptor.workspace.agentPromptPath?.trim();
522
554
  if (!configured)
@@ -557,7 +589,9 @@ export class Supervisor {
557
589
  // mode.
558
590
  this.sendEvent(running, 'notice', {
559
591
  level: 'info',
560
- text: `Project prompt loaded from ${quotePath(result.relPath)} — ${agentPromptSizeLabel(result.bytes)}, sha ${result.sha}.`,
592
+ text: resuming
593
+ ? `Project prompt re-applied from ${quotePath(result.relPath)} — ${agentPromptSizeLabel(result.bytes)}, sha ${result.sha}. A new agent process needs it again; the conversation is unchanged.`
594
+ : `Project prompt loaded from ${quotePath(result.relPath)} — ${agentPromptSizeLabel(result.bytes)}, sha ${result.sha}.`,
561
595
  });
562
596
  return { text: result.text, absPath: result.absPath };
563
597
  }
@@ -2095,7 +2129,13 @@ export class Supervisor {
2095
2129
  */
2096
2130
  const wasMidTurn = descriptor.status === 'RUNNING' || descriptor.status === 'WAITING_PERMISSION';
2097
2131
  const resumeId = descriptor.providerSessionId;
2098
- const willContinue = wasMidTurn && claimAutoResume(descriptor.id);
2132
+ // Ticket #177: `resumeId` is required, not merely nice to have. Without
2133
+ // it the relaunch starts a FRESH conversation, and `AUTO_RESUME_PROMPT`
2134
+ // — "continue from where you stopped, re-check what you were in the
2135
+ // middle of" — would be addressed to an agent that remembers none of
2136
+ // it. A process killed before it reported its session id (the SIGABRT
2137
+ // this ticket came from) leaves the row in exactly that state.
2138
+ const willContinue = wasMidTurn && Boolean(resumeId) && claimAutoResume(descriptor.id);
2099
2139
  // The note stays either way (owner's call): an interruption is a fact
2100
2140
  // about the session and must not disappear just because we recovered
2101
2141
  // from it. Only the instruction at the end changes — telling someone to
@@ -2103,7 +2143,9 @@ export class Supervisor {
2103
2143
  this.sendEvent(running, 'system_note', {
2104
2144
  text: willContinue
2105
2145
  ? 'Runner reconnected. The session was resumed — continuing the interrupted turn.'
2106
- : 'Runner reconnected. The session was resumed — send a message to continue.',
2146
+ : resumeId
2147
+ ? 'Runner reconnected. The session was resumed — send a message to continue.'
2148
+ : 'Runner reconnected, but the agent never got as far as naming its conversation, so it starts this one over. Your files, your branch and everything above are untouched.',
2107
2149
  });
2108
2150
  if (willContinue) {
2109
2151
  // Resumed through the PROVIDER session, so the agent keeps its whole
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.34.0";
1
+ export declare const RUNNER_VERSION = "0.35.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in sync with package.json by the release script (manual for now).
2
- export const RUNNER_VERSION = '0.34.0';
2
+ export const RUNNER_VERSION = '0.35.0';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge4dev/runner",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
5
5
  "homepage": "https://bridge4.dev",
6
6
  "license": "MIT",