@bridge4dev/runner 0.67.0 → 0.68.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.
@@ -33,11 +33,31 @@ export type AccountCommandReply = {
33
33
  error: string;
34
34
  result?: unknown;
35
35
  };
36
+ /**
37
+ * What became of the sessions running when an account was switched (#422 S4.1).
38
+ *
39
+ * `switched` – parked right now, so their next message starts them on the new
40
+ * account with their conversation kept; `pending` – in the middle of a turn,
41
+ * marked and parked the moment that turn ends. A turn is never killed for a
42
+ * switch.
43
+ */
44
+ export interface AccountApplyReport {
45
+ switched: number;
46
+ pending: number;
47
+ }
48
+ /**
49
+ * The supervisor's side of «apply to the open sessions» – it is the one that
50
+ * holds them. Absent in the tests that drive the commands alone; then the
51
+ * answer carries no `applied` and nothing was moved.
52
+ */
53
+ export type AccountApply = (agent: RelayAgent, accountId: string, label?: string) => AccountApplyReport;
36
54
  /** The two sign-in steps the commands drive – `AuthRelay`, or a test double. */
37
55
  export interface AccountSignIn {
38
- startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent): Promise<LoginStartResult>;
56
+ startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent, options?: {
57
+ activate?: boolean;
58
+ }): Promise<LoginStartResult>;
39
59
  submitAccountCode(code: string): Promise<AccountLoginCodeResult>;
40
60
  }
41
61
  export declare function isAccountCommand(name: string): name is AccountCommand;
42
- export declare function runAccountCommand(name: AccountCommand, args: Record<string, unknown>, signIn: AccountSignIn): Promise<AccountCommandReply>;
62
+ export declare function runAccountCommand(name: AccountCommand, args: Record<string, unknown>, signIn: AccountSignIn, apply?: AccountApply): Promise<AccountCommandReply>;
43
63
  //# sourceMappingURL=account-commands.d.ts.map
@@ -1,5 +1,5 @@
1
- import { forgetAccount, listAccounts, setActiveAccount } from './claude-homes.js';
2
- import { activateCodexAccount, forgetCodexAccount, listCodexAccounts } from './codex-accounts.js';
1
+ import { forgetAccount, listAccounts, readActiveAccountSummary, setActiveAccount, } from './claude-homes.js';
2
+ import { activateCodexAccount, codexActiveAccountSummary, forgetCodexAccount, listCodexAccounts, } from './codex-accounts.js';
3
3
  import { AccountError, MACHINE_ACCOUNT_ID, isAccountId } from './login-marks.js';
4
4
  import { maskString } from './policy.js';
5
5
  /**
@@ -32,6 +32,15 @@ export const ACCOUNT_COMMANDS = [
32
32
  * R11). Codex joined in S4 – the same five commands and the same card.
33
33
  */
34
34
  export const ACCOUNT_AGENTS = ['claude', 'codex'];
35
+ /**
36
+ * «And make it the one new sessions start on», the window's tick (S4.1).
37
+ *
38
+ * Absent – true: that is what every sign-in did before the tick existed, and an
39
+ * older API sends no such argument.
40
+ */
41
+ function activateArg(args) {
42
+ return args['activate'] === false ? { activate: false } : {};
43
+ }
35
44
  export function isAccountCommand(name) {
36
45
  return ACCOUNT_COMMANDS.includes(name);
37
46
  }
@@ -46,10 +55,40 @@ function refusal(error) {
46
55
  function accountIdArg(value) {
47
56
  return value === MACHINE_ACCOUNT_ID || isAccountId(value) ? value : null;
48
57
  }
49
- export async function runAccountCommand(name, args, signIn) {
58
+ /**
59
+ * Who the account now active is, for the note the parked sessions are given.
60
+ *
61
+ * Read locally and never allowed to fail the switch itself: a label is a
62
+ * courtesy, the switch is the work.
63
+ */
64
+ function labelOf(agent, id) {
65
+ try {
66
+ const summary = agent === 'codex' ? codexActiveAccountSummary(id) : readActiveAccountSummary(id);
67
+ return summary.email;
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ /**
74
+ * «Apply to the sessions already running», when the caller asked for it (S4.1).
75
+ *
76
+ * An ARGUMENT of an existing command rather than a name of its own, which §8
77
+ * otherwise forbids – and safe here for one reason: a runner that does not know
78
+ * it still switches the account, it simply leaves the open sessions where they
79
+ * are. The API tells the two apart by this very field: an answer without
80
+ * `applied` to a request that asked for it IS «that machine is older», and the
81
+ * window says so instead of claiming sessions were moved.
82
+ */
83
+ function appliedTo(agent, active, args, apply) {
84
+ if (args['applySessions'] !== true || !apply)
85
+ return {};
86
+ return { applied: apply(agent, active, labelOf(agent, active)) };
87
+ }
88
+ export async function runAccountCommand(name, args, signIn, apply) {
50
89
  const agent = args['agent'];
51
90
  if (agent === 'codex')
52
- return runCodexAccountCommand(name, args, signIn);
91
+ return runCodexAccountCommand(name, args, signIn, apply);
53
92
  if (agent !== 'claude')
54
93
  return { ok: false, error: 'agent must be claude or codex' };
55
94
  try {
@@ -63,7 +102,10 @@ export async function runAccountCommand(name, args, signIn) {
63
102
  if (target !== 'machine' && target !== 'saved') {
64
103
  return { ok: false, error: 'target must be machine or saved' };
65
104
  }
66
- return { ok: true, result: await signIn.startAccountLogin(target) };
105
+ return {
106
+ ok: true,
107
+ result: await signIn.startAccountLogin(target, 'claude', activateArg(args)),
108
+ };
67
109
  }
68
110
  case 'agent_account_login_code': {
69
111
  const code = args['code'];
@@ -79,7 +121,8 @@ export async function runAccountCommand(name, args, signIn) {
79
121
  return { ok: false, error: 'no such account on this server' };
80
122
  // The usage cache is keyed by subscription, so a switch leaves it alone:
81
123
  // the new row's figures are already its own (R16, §8).
82
- return { ok: true, result: { active: setActiveAccount(id) } };
124
+ const active = setActiveAccount(id);
125
+ return { ok: true, result: { active, ...appliedTo('claude', active, args, apply) } };
83
126
  }
84
127
  case 'agent_account_forget': {
85
128
  const id = accountIdArg(args['id']);
@@ -101,7 +144,7 @@ export async function runAccountCommand(name, args, signIn) {
101
144
  * device sign-in finishes on the provider's page – there is no code to send; the
102
145
  * window learns the result from the list (§8).
103
146
  */
104
- async function runCodexAccountCommand(name, args, signIn) {
147
+ async function runCodexAccountCommand(name, args, signIn, apply) {
105
148
  try {
106
149
  switch (name) {
107
150
  case 'agent_accounts':
@@ -111,7 +154,10 @@ async function runCodexAccountCommand(name, args, signIn) {
111
154
  if (target !== 'machine' && target !== 'saved') {
112
155
  return { ok: false, error: 'target must be machine or saved' };
113
156
  }
114
- return { ok: true, result: await signIn.startAccountLogin(target, 'codex') };
157
+ return {
158
+ ok: true,
159
+ result: await signIn.startAccountLogin(target, 'codex', activateArg(args)),
160
+ };
115
161
  }
116
162
  case 'agent_account_login_code': {
117
163
  const result = {
@@ -124,7 +170,8 @@ async function runCodexAccountCommand(name, args, signIn) {
124
170
  const id = accountIdArg(args['id']);
125
171
  if (!id)
126
172
  return { ok: false, error: 'no such account on this server' };
127
- return { ok: true, result: { active: activateCodexAccount(id) } };
173
+ const active = activateCodexAccount(id);
174
+ return { ok: true, result: { active, ...appliedTo('codex', active, args, apply) } };
128
175
  }
129
176
  case 'agent_account_forget': {
130
177
  const id = accountIdArg(args['id']);
@@ -179,5 +179,6 @@ export declare function discardAbandonedCodexStagingHomes(): number;
179
179
  */
180
180
  export declare function adoptLoginResult(stagingDir: string, options?: {
181
181
  replaceActive?: boolean;
182
+ activate?: boolean;
182
183
  }): StoredCodexLogin;
183
184
  //# sourceMappingURL=codex-home.d.ts.map
@@ -769,7 +769,10 @@ export function adoptLoginResult(stagingDir, options = {}) {
769
769
  throw new AccountError('no sign-in in progress – start again');
770
770
  try {
771
771
  return storeCodexLogin(path.join(stagingDir, AUTH_FILENAME), {
772
- activate: true,
772
+ // `activate` is the window's «and make it the one in use», on unless the
773
+ // person turned it off (S4.1); `replaceActive` is the one-login door,
774
+ // where a sign-in has always taken the place of the login in use (D27).
775
+ activate: options.activate !== false,
773
776
  ...(options.replaceActive ? { replaceActive: true } : {}),
774
777
  });
775
778
  }
@@ -105,7 +105,9 @@ export declare class AuthRelay {
105
105
  * (it prints an inference-only token, D1), so a «saved account» made from it
106
106
  * would be a row with no login. `machine` keeps the fallback it always had.
107
107
  */
108
- startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent): Promise<LoginStartResult>;
108
+ startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent, options?: {
109
+ activate?: boolean;
110
+ }): Promise<LoginStartResult>;
109
111
  /** The CLI and the pty helper are both there – or a sentence saying which is not. */
110
112
  private assertCanRun;
111
113
  private startWith;
@@ -226,7 +226,10 @@ export class AuthRelay {
226
226
  * (it prints an inference-only token, D1), so a «saved account» made from it
227
227
  * would be a row with no login. `machine` keeps the fallback it always had.
228
228
  */
229
- async startAccountLogin(target, agent = 'claude') {
229
+ async startAccountLogin(target, agent = 'claude', options = {}) {
230
+ // Default true: that is what a sign-in has always done, and it is what an
231
+ // older API – which sends no such argument – must keep getting.
232
+ const activate = options.activate !== false;
230
233
  if (agent === 'codex') {
231
234
  // R13: the machine row of Codex is the host user's own `~/.codex/auth.json`,
232
235
  // which DevBridge has never written and does not start writing. A sign-in
@@ -239,13 +242,14 @@ export class AuthRelay {
239
242
  return this.startWith('codex', this.commands.codex ?? '', false, {
240
243
  flow: 'account',
241
244
  target: 'saved',
245
+ activate,
242
246
  });
243
247
  }
244
248
  this.cancel();
245
249
  this.assertCanRun('claude');
246
250
  const command = this.commands.claude ?? '';
247
251
  try {
248
- return await this.startWith('claude', command, false, { flow: 'account', target });
252
+ return await this.startWith('claude', command, false, { flow: 'account', target, activate });
249
253
  }
250
254
  catch (error) {
251
255
  if (!looksLikeUnsupportedSubcommand(String(error)))
@@ -254,7 +258,11 @@ export class AuthRelay {
254
258
  throw new Error('the Claude CLI on this server is too old to sign in to a saved account – update Claude Code and try again', { cause: error });
255
259
  }
256
260
  log.warn('auth-relay: this claude has no `auth login` — falling back to setup-token');
257
- return this.startWith('claude', CLAUDE_LEGACY_LOGIN, true, { flow: 'account', target });
261
+ return this.startWith('claude', CLAUDE_LEGACY_LOGIN, true, {
262
+ flow: 'account',
263
+ target,
264
+ activate,
265
+ });
258
266
  }
259
267
  }
260
268
  /** The CLI and the pty helper are both there – or a sentence saying which is not. */
@@ -310,6 +318,7 @@ export class AuthRelay {
310
318
  captureToken,
311
319
  flow: door.flow,
312
320
  target: door.target,
321
+ activate: door.activate !== false,
313
322
  claudeStaging,
314
323
  codexStaging: stagingHome,
315
324
  exchanging: false,
@@ -351,7 +360,12 @@ export class AuthRelay {
351
360
  // use instead of adding a row beside it: with the organization's option
352
361
  // off, «a token overwrites the token» is what a sign-in has always meant
353
362
  // (D27), and DevBridge writes no machine login for Codex to overwrite.
354
- const stored = adoptCodexLogin(staging, { replaceActive: relay.flow === 'legacy' });
363
+ const stored = adoptCodexLogin(staging, {
364
+ replaceActive: relay.flow === 'legacy',
365
+ // «Log in, but do not switch to it» (S4.1): the row is stored and
366
+ // listed, the link the sessions start from stays where it is.
367
+ activate: relay.activate,
368
+ });
355
369
  log.info('codex: device login completed — credential stored', {
356
370
  account: stored.id,
357
371
  replaced: Boolean(stored.replaced),
@@ -452,7 +466,7 @@ export class AuthRelay {
452
466
  };
453
467
  }
454
468
  try {
455
- const adopted = await adoptClaudeLogin(relay.claudeStaging);
469
+ const adopted = await adoptClaudeLogin(relay.claudeStaging, { activate: relay.activate });
456
470
  // Adopted: the staging path no longer exists, nothing left to remove.
457
471
  relay.claudeStaging = null;
458
472
  clearAgentAuthFailure('claude', adopted.account.id);
@@ -574,7 +588,8 @@ export class AuthRelay {
574
588
  log.info('auth-relay: stored a long-lived Claude token for this runner');
575
589
  clearAgentAuthFailure('claude');
576
590
  forgetClaudeUsage();
577
- activateMachineLogin();
591
+ if (relay.activate)
592
+ activateMachineLogin();
578
593
  return { ok: true, detail: 'signed in with a long-lived token stored on this server' };
579
594
  }
580
595
  // `claude auth login` writes the credential just before it exits; give the
@@ -588,7 +603,8 @@ export class AuthRelay {
588
603
  if (status.status === 'ok') {
589
604
  clearAgentAuthFailure('claude');
590
605
  forgetClaudeUsage();
591
- activateMachineLogin();
606
+ if (relay.activate)
607
+ activateMachineLogin();
592
608
  return { ok: true };
593
609
  }
594
610
  await sleep(300);
@@ -407,7 +407,9 @@ export interface AdoptResult {
407
407
  * BEFORE a new home is moved into place, so a failed write never leaves a login
408
408
  * nobody can list or forget. The signed-in row becomes active (R15).
409
409
  */
410
- export declare function adoptLoginResult(stagingDir: string): Promise<AdoptResult>;
410
+ export declare function adoptLoginResult(stagingDir: string, options?: {
411
+ activate?: boolean;
412
+ }): Promise<AdoptResult>;
411
413
  /**
412
414
  * Bring a home that already holds a login into the store – by rename (D17, R7).
413
415
  *
@@ -1347,7 +1347,12 @@ export function discardStagingHome(dir) {
1347
1347
  * BEFORE a new home is moved into place, so a failed write never leaves a login
1348
1348
  * nobody can list or forget. The signed-in row becomes active (R15).
1349
1349
  */
1350
- export async function adoptLoginResult(stagingDir) {
1350
+ export async function adoptLoginResult(stagingDir, options = {}) {
1351
+ // «And make it the one new sessions start on» – the window's own tick, on
1352
+ // unless the person turned it off (S4.1). Off: the row is stored and listed,
1353
+ // and the active account does not move. Default true, because that is what a
1354
+ // sign-in has always done and what an older API still asks for.
1355
+ const activate = options.activate !== false;
1351
1356
  if (!isStagingDir(stagingDir) || !lstatOrNull(stagingDir)?.isDirectory()) {
1352
1357
  throw new AccountError('no sign-in in progress – start again');
1353
1358
  }
@@ -1395,10 +1400,12 @@ export async function adoptLoginResult(stagingDir) {
1395
1400
  });
1396
1401
  throw error;
1397
1402
  }
1398
- updateAgentAuth((file) => {
1399
- file.claudeActiveAccount = id;
1400
- });
1401
- log.info('claude-homes: a new account was added', { account: id });
1403
+ if (activate) {
1404
+ updateAgentAuth((file) => {
1405
+ file.claudeActiveAccount = id;
1406
+ });
1407
+ }
1408
+ log.info('claude-homes: a new account was added', { account: id, active: activate });
1402
1409
  return { account: cardOf(id), active: activeAccountId() };
1403
1410
  }
1404
1411
  const home = accountHome(same.id);
@@ -1425,7 +1432,11 @@ export async function adoptLoginResult(stagingDir) {
1425
1432
  record.lastSeenIdentity = stamped;
1426
1433
  delete record.loginExpiredAt;
1427
1434
  }
1428
- file.claudeActiveAccount = same.id;
1435
+ // The row that signed in again is the SAME row: leaving the pointer alone
1436
+ // when the tick is off keeps whatever was active active – including this
1437
+ // row, when it already was.
1438
+ if (activate)
1439
+ file.claudeActiveAccount = same.id;
1429
1440
  });
1430
1441
  // A fresh login outranks anything remembered about the one it replaced – on
1431
1442
  // disk above, and in this process's memory of refusals here.
@@ -1144,6 +1144,36 @@ export declare class Supervisor {
1144
1144
  * rewound (ticket #126).
1145
1145
  */
1146
1146
  private park;
1147
+ /**
1148
+ * Move the sessions running right now onto the account just made active (#422 S4.1).
1149
+ *
1150
+ * A process reads its account ONCE, at its start: `CLAUDE_CONFIG_DIR` for
1151
+ * Claude, the link `codex-home/auth.json` resolves to for Codex. Neither can
1152
+ * be changed under a live process, so «apply to the open sessions» is parking
1153
+ * them – the conversation is kept (`--resume` / `thread/resume`) and the next
1154
+ * message starts a process on the account the machine is now set to.
1155
+ *
1156
+ * A session in the middle of a turn is not killed for this: it is marked and
1157
+ * parked at the end of its turn (`applyPendingAccountSwitch`). Which is why
1158
+ * the answer counts the two separately – the window says so in words.
1159
+ */
1160
+ private applyAccountToSessions;
1161
+ /**
1162
+ * Park a session because the account under it changed – its own words.
1163
+ *
1164
+ * `quiet`, then a note of its own: «the runner switched to another session»
1165
+ * is simply untrue here, and this is the one thing the person needs to read
1166
+ * to know why their session is waiting for a message (the same reason #126
1167
+ * gave the rewind its own wording).
1168
+ */
1169
+ private parkForAccount;
1170
+ /**
1171
+ * The turn is over – carry out a switch that arrived while it was running.
1172
+ *
1173
+ * Still not parkable (a card is open, a question is waiting): the mark stays
1174
+ * and the next end of a turn tries again. Nothing here ever kills a turn.
1175
+ */
1176
+ private applyPendingAccountSwitch;
1147
1177
  /**
1148
1178
  * The agent is producing output while this runner still says a human is
1149
1179
  * expected — put `lastReported` back to RUNNING (ticket #185).
@@ -19,7 +19,7 @@ import { applySession, gitBranches, gitCommit, gitDiff, gitLog, gitPush, gitRefs
19
19
  import { fsView } from './fsview.js';
20
20
  import { publishFile } from './file-publish.js';
21
21
  import { agentAuthStatuses, AuthRelay, clearAgentAuthFailure, noteAgentAuthFailure, } from './auth-relay.js';
22
- import { isAccountCommand, runAccountCommand } from './account-commands.js';
22
+ import { isAccountCommand, runAccountCommand, } from './account-commands.js';
23
23
  import { isSupervisedProcess, selfUpdate } from './self-update.js';
24
24
  import { installAgent } from './agent-install.js';
25
25
  import { autoUpdateRefusal, claimAgentAutoUpdate } from './agent-auto-update.js';
@@ -3337,7 +3337,11 @@ export class Supervisor {
3337
3337
  activeMs: Supervisor.spentMs(running),
3338
3338
  errorMessage: event.errorMessage ?? 'Agent turn failed',
3339
3339
  });
3340
+ return;
3340
3341
  }
3342
+ // The turn is over – so an account switch that arrived while it was running
3343
+ // can be carried out now, without costing that turn (#422 S4.1).
3344
+ this.applyPendingAccountSwitch(running);
3341
3345
  }
3342
3346
  /**
3343
3347
  * Hold a turn that produced nothing, in case it was never a turn (#300).
@@ -3578,6 +3582,93 @@ export class Supervisor {
3578
3582
  }
3579
3583
  running.session.stop('session_parked');
3580
3584
  }
3585
+ /**
3586
+ * Move the sessions running right now onto the account just made active (#422 S4.1).
3587
+ *
3588
+ * A process reads its account ONCE, at its start: `CLAUDE_CONFIG_DIR` for
3589
+ * Claude, the link `codex-home/auth.json` resolves to for Codex. Neither can
3590
+ * be changed under a live process, so «apply to the open sessions» is parking
3591
+ * them – the conversation is kept (`--resume` / `thread/resume`) and the next
3592
+ * message starts a process on the account the machine is now set to.
3593
+ *
3594
+ * A session in the middle of a turn is not killed for this: it is marked and
3595
+ * parked at the end of its turn (`applyPendingAccountSwitch`). Which is why
3596
+ * the answer counts the two separately – the window says so in words.
3597
+ */
3598
+ applyAccountToSessions(agent, accountId, label) {
3599
+ let switched = 0;
3600
+ let pending = 0;
3601
+ for (const running of this.sessions.values()) {
3602
+ if (agentByDbValue(running.descriptor.agent)?.wireKey !== agent)
3603
+ continue;
3604
+ const session = running.session;
3605
+ // Parked already: whatever starts it next reads the active account then.
3606
+ if (!session)
3607
+ continue;
3608
+ // Already there, or an adapter that does not name an account at all – a
3609
+ // switch has nothing to do to it, and a stale mark must not outlive it.
3610
+ if (!session.accountId || session.accountId === accountId) {
3611
+ delete running.accountSwitchPending;
3612
+ continue;
3613
+ }
3614
+ if (this.isParkable(running)) {
3615
+ delete running.accountSwitchPending;
3616
+ this.parkForAccount(running, label);
3617
+ switched += 1;
3618
+ }
3619
+ else {
3620
+ running.accountSwitchPending = { id: accountId, ...(label ? { label } : {}) };
3621
+ pending += 1;
3622
+ }
3623
+ }
3624
+ if (switched || pending) {
3625
+ log.info('supervisor: the account switch reached the open sessions', {
3626
+ agent,
3627
+ account: accountId,
3628
+ switched,
3629
+ pending,
3630
+ });
3631
+ }
3632
+ return { switched, pending };
3633
+ }
3634
+ /**
3635
+ * Park a session because the account under it changed – its own words.
3636
+ *
3637
+ * `quiet`, then a note of its own: «the runner switched to another session»
3638
+ * is simply untrue here, and this is the one thing the person needs to read
3639
+ * to know why their session is waiting for a message (the same reason #126
3640
+ * gave the rewind its own wording).
3641
+ */
3642
+ parkForAccount(running, label) {
3643
+ this.sendEvent(running, 'system_note', {
3644
+ // The word the dashboard reads to tell «parked» from «your turn» (#124) –
3645
+ // the state IS parked; only the reason differs.
3646
+ code: 'session_parked',
3647
+ text: label
3648
+ ? `The agent account on this server was switched to ${label} — send a message and this session carries on under it, with everything above kept.`
3649
+ : 'The agent account on this server was switched — send a message and this session carries on under the new one, with everything above kept.',
3650
+ });
3651
+ this.park(running, { quiet: true });
3652
+ }
3653
+ /**
3654
+ * The turn is over – carry out a switch that arrived while it was running.
3655
+ *
3656
+ * Still not parkable (a card is open, a question is waiting): the mark stays
3657
+ * and the next end of a turn tries again. Nothing here ever kills a turn.
3658
+ */
3659
+ applyPendingAccountSwitch(running) {
3660
+ const wanted = running.accountSwitchPending;
3661
+ if (!wanted)
3662
+ return;
3663
+ if (!running.session || running.session.accountId === wanted.id) {
3664
+ delete running.accountSwitchPending;
3665
+ return;
3666
+ }
3667
+ if (!this.isParkable(running))
3668
+ return;
3669
+ delete running.accountSwitchPending;
3670
+ this.parkForAccount(running, wanted.label);
3671
+ }
3581
3672
  /**
3582
3673
  * The agent is producing output while this runner still says a human is
3583
3674
  * expected — put `lastReported` back to RUNNING (ticket #185).
@@ -7494,7 +7585,11 @@ export class Supervisor {
7494
7585
  // #422: the five account commands – their own module, one door each
7495
7586
  // over `claude-homes.ts` (plan §8).
7496
7587
  if (isAccountCommand(frame.name)) {
7497
- return void reply(await runAccountCommand(frame.name, frame.args ?? {}, this.authRelay));
7588
+ return void reply(
7589
+ // The fourth argument is «apply to the sessions already running»
7590
+ // (#422 S4.1): the commands know the accounts, this class knows
7591
+ // the sessions, so the switch reaches them from here.
7592
+ await runAccountCommand(frame.name, frame.args ?? {}, this.authRelay, (...args) => this.applyAccountToSessions(...args)));
7498
7593
  }
7499
7594
  return void reply({ ok: false, error: `Unknown command` });
7500
7595
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.67.0";
1
+ export declare const RUNNER_VERSION = "0.68.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.67.0';
2
+ export const RUNNER_VERSION = '0.68.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.67.0",
3
+ "version": "0.68.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",