@bridge4dev/runner 0.69.1 → 0.70.1

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/index.js CHANGED
@@ -397,6 +397,17 @@ function runnerCapabilities(apiUrlOverride) {
397
397
  * machine in the fleet is up to date.
398
398
  */
399
399
  agentOutsideFolder: true,
400
+ /**
401
+ * «Tell the agent who is writing»: the name the API attaches to a message
402
+ * (`author` on `deliver_message` and `session_message`, `promptAuthor` on
403
+ * the descriptor) goes on top of the agent's prompt, never into the feed.
404
+ *
405
+ * Announced for the settings card only, and as a warning rather than a
406
+ * refusal, like `agentOutsideFolder` above it: an older runner drops the
407
+ * field unread and the message still arrives — just unsigned — so a
408
+ * manager can record the decision before the last machine is current.
409
+ */
410
+ messageAuthors: true,
400
411
  /**
401
412
  * #137: this runner can read a repository that has no commits yet.
402
413
  *
package/dist/journal.d.ts CHANGED
@@ -30,6 +30,13 @@ export interface PendingMessage {
30
30
  * those cannot be recalled, which is correct — they predate the feature.
31
31
  */
32
32
  seq?: number;
33
+ /**
34
+ * Who typed it, as DevBridge shows them — present only when the project has
35
+ * «Tell the agent who is writing» switched on. Held with the words so a
36
+ * message that waited out a restart is still signed for the agent when it
37
+ * finally goes (`message-author.ts`).
38
+ */
39
+ author?: string;
33
40
  }
34
41
  export interface PendingAttachment {
35
42
  id: string;
@@ -140,7 +147,7 @@ export declare class SessionJournal {
140
147
  * it is queued in memory, so the ordering is "on disk, then held" — a crash
141
148
  * between the two costs a duplicate delivery at worst, never a lost message.
142
149
  */
143
- appendPending(text: string, attachments?: PendingAttachment[], seq?: number): PendingMessage;
150
+ appendPending(text: string, attachments?: PendingAttachment[], seq?: number, author?: string): PendingMessage;
144
151
  /** The message reached an agent — stop replaying it after a restart. */
145
152
  resolvePending(id: string): void;
146
153
  /**
package/dist/journal.js CHANGED
@@ -97,6 +97,7 @@ export class SessionJournal {
97
97
  text: parsed.text,
98
98
  ...(parsed.attachments?.length ? { attachments: parsed.attachments } : {}),
99
99
  ...(typeof parsed.seq === 'number' ? { seq: parsed.seq } : {}),
100
+ ...(typeof parsed.author === 'string' ? { author: parsed.author } : {}),
100
101
  });
101
102
  // Ids are `p<n>`; keep the counter above whatever the file holds so a
102
103
  // restarted runner cannot mint an id that is already in flight.
@@ -157,6 +158,7 @@ export class SessionJournal {
157
158
  text: record.text,
158
159
  ...(record.attachments?.length ? { attachments: record.attachments } : {}),
159
160
  ...(record.seq === undefined ? {} : { seq: record.seq }),
161
+ ...(record.author === undefined ? {} : { author: record.author }),
160
162
  ts: new Date().toISOString(),
161
163
  });
162
164
  }
@@ -294,13 +296,14 @@ export class SessionJournal {
294
296
  * it is queued in memory, so the ordering is "on disk, then held" — a crash
295
297
  * between the two costs a duplicate delivery at worst, never a lost message.
296
298
  */
297
- appendPending(text, attachments, seq) {
299
+ appendPending(text, attachments, seq, author) {
298
300
  const id = `p${this.pendingCounter++}`;
299
301
  const record = {
300
302
  id,
301
303
  text,
302
304
  ...(attachments?.length ? { attachments } : {}),
303
305
  ...(seq === undefined ? {} : { seq }),
306
+ ...(author === undefined ? {} : { author }),
304
307
  };
305
308
  this.write({
306
309
  kind: 'pending',
@@ -308,6 +311,7 @@ export class SessionJournal {
308
311
  text,
309
312
  ...(attachments?.length ? { attachments } : {}),
310
313
  ...(seq === undefined ? {} : { seq }),
314
+ ...(author === undefined ? {} : { author }),
311
315
  ts: new Date().toISOString(),
312
316
  });
313
317
  this.pendingById.set(id, record);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Who typed this — for the agent, not for the feed.
3
+ *
4
+ * ## Why the agent needs it
5
+ *
6
+ * Several people write into one session: the developer, a colleague forwarding
7
+ * a client's email, the platform owner stepping in to help. The runner echoes
8
+ * every message into the feed as `role: 'user'` and the API stamps the author
9
+ * onto that echo for people to see — but the AGENT got the bare words, and
10
+ * could not tell one person from another. A project prompt that says «if Andy
11
+ * writes, do not start development — say you are waiting for Dima» had nothing
12
+ * to go on.
13
+ *
14
+ * ## What it is, and what it is not
15
+ *
16
+ * A line the platform puts on top of the composed prompt, named after the
17
+ * person as DevBridge shows them. The feed never sees it: `acceptUserMessage`
18
+ * echoes the text the person typed, and this runs on the way to the agent, the
19
+ * same place the attachment paths are added. It is a hint to the model, not an
20
+ * access control — the project's real permissions are its roles, and the line
21
+ * carries no authority of its own. The API decides per message whether to send
22
+ * a name at all (the project setting «Tell the agent who is writing»); the
23
+ * runner adds the line whenever one arrives and never invents one.
24
+ *
25
+ * The name comes off the wire and is treated as such: folded onto one line,
26
+ * trimmed, and an empty one means no line at all.
27
+ */
28
+ export declare function signMessage(text: string, author: string | undefined): string;
29
+ //# sourceMappingURL=message-author.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Who typed this — for the agent, not for the feed.
3
+ *
4
+ * ## Why the agent needs it
5
+ *
6
+ * Several people write into one session: the developer, a colleague forwarding
7
+ * a client's email, the platform owner stepping in to help. The runner echoes
8
+ * every message into the feed as `role: 'user'` and the API stamps the author
9
+ * onto that echo for people to see — but the AGENT got the bare words, and
10
+ * could not tell one person from another. A project prompt that says «if Andy
11
+ * writes, do not start development — say you are waiting for Dima» had nothing
12
+ * to go on.
13
+ *
14
+ * ## What it is, and what it is not
15
+ *
16
+ * A line the platform puts on top of the composed prompt, named after the
17
+ * person as DevBridge shows them. The feed never sees it: `acceptUserMessage`
18
+ * echoes the text the person typed, and this runs on the way to the agent, the
19
+ * same place the attachment paths are added. It is a hint to the model, not an
20
+ * access control — the project's real permissions are its roles, and the line
21
+ * carries no authority of its own. The API decides per message whether to send
22
+ * a name at all (the project setting «Tell the agent who is writing»); the
23
+ * runner adds the line whenever one arrives and never invents one.
24
+ *
25
+ * The name comes off the wire and is treated as such: folded onto one line,
26
+ * trimmed, and an empty one means no line at all.
27
+ */
28
+ export function signMessage(text, author) {
29
+ const name = (author ?? '').replace(/\s+/g, ' ').trim();
30
+ if (name === '')
31
+ return text;
32
+ return `[Message from ${name}]\n\n${text}`;
33
+ }
34
+ //# sourceMappingURL=message-author.js.map
@@ -1,6 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import type { HostLoadFrame } from './host-load.js';
3
3
  import type { SessionLimitsFrame } from './session-limits.js';
4
+ /**
5
+ * The longest name the wire accepts for «who typed this». A display name from a
6
+ * profile, not a document — anything longer is not a name, and the field is a
7
+ * nicety: over the limit it is dropped and the message goes on without it.
8
+ */
9
+ export declare const MESSAGE_AUTHOR_MAX = 200;
4
10
  export declare const SessionDescriptorSchema: z.ZodObject<{
5
11
  id: z.ZodString;
6
12
  kind: z.ZodEnum<["TICKET", "CHAT"]>;
@@ -71,6 +77,18 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
71
77
  * arrives as `undefined`, which is the same as «auto-update is off».
72
78
  */
73
79
  agentLatestVersion: z.ZodCatch<z.ZodOptional<z.ZodString>>;
80
+ /**
81
+ * Who handed over the first message — the person who created the session, as
82
+ * DevBridge shows them. Sent only when the project has «Tell the agent who is
83
+ * writing» switched on and the prompt is not empty; `composeInitialPrompt`
84
+ * puts the name on top of the whole first message (`message-author.ts`).
85
+ *
86
+ * Optional, and absent means «no name»: an older API never sends it, and a
87
+ * project with the setting off gets exactly the first message it always had.
88
+ * `.catch(undefined)` like its neighbours — a bad value costs the signature,
89
+ * never the descriptor, never the `hello_ack` it rides in (QA-100 MAJOR-1).
90
+ */
91
+ promptAuthor: z.ZodCatch<z.ZodOptional<z.ZodString>>;
74
92
  workspace: z.ZodObject<{
75
93
  id: z.ZodString;
76
94
  path: z.ZodString;
@@ -277,6 +295,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
277
295
  pausedUntil?: string | null | undefined;
278
296
  backgroundTasks?: number | undefined;
279
297
  agentLatestVersion?: string | undefined;
298
+ promptAuthor?: string | undefined;
280
299
  skipAgentPrompt?: boolean | undefined;
281
300
  branchHint?: string | undefined;
282
301
  branchPlan?: {
@@ -328,6 +347,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
328
347
  pausedUntil?: unknown;
329
348
  backgroundTasks?: unknown;
330
349
  agentLatestVersion?: unknown;
350
+ promptAuthor?: unknown;
331
351
  skipAgentPrompt?: unknown;
332
352
  branchHint?: unknown;
333
353
  branchPlan?: unknown;
@@ -362,6 +382,14 @@ export declare const QuestionAnswerShape: {
362
382
  notes?: string | undefined;
363
383
  }>, "many">>;
364
384
  readonly text: z.ZodOptional<z.ZodString>;
385
+ /**
386
+ * Who typed `text` — sent only when the project has «Tell the agent who is
387
+ * writing» on. Signs the free text of a «discuss» reply (words a person typed
388
+ * into an open card are a message like any other) and the words rescued when
389
+ * the card turns out to be dead; a pick stays a pick. `.catch(undefined)`
390
+ * like the message author: a bad name costs the signature, not the answer.
391
+ */
392
+ readonly author: z.ZodCatch<z.ZodOptional<z.ZodString>>;
365
393
  };
366
394
  /** The `answer_question` command's arguments — the shape above on its own. */
367
395
  export declare const QuestionAnswerArgsSchema: z.ZodObject<{
@@ -384,10 +412,19 @@ export declare const QuestionAnswerArgsSchema: z.ZodObject<{
384
412
  notes?: string | undefined;
385
413
  }>, "many">>;
386
414
  readonly text: z.ZodOptional<z.ZodString>;
415
+ /**
416
+ * Who typed `text` — sent only when the project has «Tell the agent who is
417
+ * writing» on. Signs the free text of a «discuss» reply (words a person typed
418
+ * into an open card are a message like any other) and the words rescued when
419
+ * the card turns out to be dead; a pick stays a pick. `.catch(undefined)`
420
+ * like the message author: a bad name costs the signature, not the answer.
421
+ */
422
+ readonly author: z.ZodCatch<z.ZodOptional<z.ZodString>>;
387
423
  }, "strip", z.ZodTypeAny, {
388
424
  action: "answer" | "discuss";
389
425
  askId: string;
390
426
  text?: string | undefined;
427
+ author?: string | undefined;
391
428
  answers?: {
392
429
  values: string[];
393
430
  questionId: string;
@@ -398,6 +435,7 @@ export declare const QuestionAnswerArgsSchema: z.ZodObject<{
398
435
  action: "answer" | "discuss";
399
436
  askId: string;
400
437
  text?: string | undefined;
438
+ author?: unknown;
401
439
  answers?: {
402
440
  values: string[];
403
441
  questionId: string;
@@ -432,9 +470,18 @@ export declare const DeliverMessageArgsSchema: z.ZodObject<{
432
470
  mimeType: string;
433
471
  fileSize: number;
434
472
  }>, "many">>>;
473
+ /**
474
+ * Who typed it, as DevBridge shows them — present only when the project has
475
+ * «Tell the agent who is writing» switched on. Goes on top of the composed
476
+ * prompt (`message-author.ts`), never into the feed echo. `.catch(undefined)`
477
+ * for the same reason attachments have it: a malformed name must cost the
478
+ * signature, never the sentence the person typed.
479
+ */
480
+ author: z.ZodCatch<z.ZodOptional<z.ZodString>>;
435
481
  }, "strip", z.ZodTypeAny, {
436
482
  text: string;
437
483
  messageId: string;
484
+ author?: string | undefined;
438
485
  attachments?: {
439
486
  id: string;
440
487
  fileName: string;
@@ -444,6 +491,7 @@ export declare const DeliverMessageArgsSchema: z.ZodObject<{
444
491
  }, {
445
492
  text: string;
446
493
  messageId: string;
494
+ author?: unknown;
447
495
  attachments?: unknown;
448
496
  }>;
449
497
  export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
@@ -521,6 +569,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
521
569
  * arrives as `undefined`, which is the same as «auto-update is off».
522
570
  */
523
571
  agentLatestVersion: z.ZodCatch<z.ZodOptional<z.ZodString>>;
572
+ /**
573
+ * Who handed over the first message — the person who created the session, as
574
+ * DevBridge shows them. Sent only when the project has «Tell the agent who is
575
+ * writing» switched on and the prompt is not empty; `composeInitialPrompt`
576
+ * puts the name on top of the whole first message (`message-author.ts`).
577
+ *
578
+ * Optional, and absent means «no name»: an older API never sends it, and a
579
+ * project with the setting off gets exactly the first message it always had.
580
+ * `.catch(undefined)` like its neighbours — a bad value costs the signature,
581
+ * never the descriptor, never the `hello_ack` it rides in (QA-100 MAJOR-1).
582
+ */
583
+ promptAuthor: z.ZodCatch<z.ZodOptional<z.ZodString>>;
524
584
  workspace: z.ZodObject<{
525
585
  id: z.ZodString;
526
586
  path: z.ZodString;
@@ -727,6 +787,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
727
787
  pausedUntil?: string | null | undefined;
728
788
  backgroundTasks?: number | undefined;
729
789
  agentLatestVersion?: string | undefined;
790
+ promptAuthor?: string | undefined;
730
791
  skipAgentPrompt?: boolean | undefined;
731
792
  branchHint?: string | undefined;
732
793
  branchPlan?: {
@@ -778,6 +839,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
778
839
  pausedUntil?: unknown;
779
840
  backgroundTasks?: unknown;
780
841
  agentLatestVersion?: unknown;
842
+ promptAuthor?: unknown;
781
843
  skipAgentPrompt?: unknown;
782
844
  branchHint?: unknown;
783
845
  branchPlan?: unknown;
@@ -827,6 +889,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
827
889
  pausedUntil?: string | null | undefined;
828
890
  backgroundTasks?: number | undefined;
829
891
  agentLatestVersion?: string | undefined;
892
+ promptAuthor?: string | undefined;
830
893
  skipAgentPrompt?: boolean | undefined;
831
894
  branchHint?: string | undefined;
832
895
  branchPlan?: {
@@ -884,6 +947,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
884
947
  pausedUntil?: unknown;
885
948
  backgroundTasks?: unknown;
886
949
  agentLatestVersion?: unknown;
950
+ promptAuthor?: unknown;
887
951
  skipAgentPrompt?: unknown;
888
952
  branchHint?: unknown;
889
953
  branchPlan?: unknown;
@@ -990,6 +1054,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
990
1054
  * arrives as `undefined`, which is the same as «auto-update is off».
991
1055
  */
992
1056
  agentLatestVersion: z.ZodCatch<z.ZodOptional<z.ZodString>>;
1057
+ /**
1058
+ * Who handed over the first message — the person who created the session, as
1059
+ * DevBridge shows them. Sent only when the project has «Tell the agent who is
1060
+ * writing» switched on and the prompt is not empty; `composeInitialPrompt`
1061
+ * puts the name on top of the whole first message (`message-author.ts`).
1062
+ *
1063
+ * Optional, and absent means «no name»: an older API never sends it, and a
1064
+ * project with the setting off gets exactly the first message it always had.
1065
+ * `.catch(undefined)` like its neighbours — a bad value costs the signature,
1066
+ * never the descriptor, never the `hello_ack` it rides in (QA-100 MAJOR-1).
1067
+ */
1068
+ promptAuthor: z.ZodCatch<z.ZodOptional<z.ZodString>>;
993
1069
  workspace: z.ZodObject<{
994
1070
  id: z.ZodString;
995
1071
  path: z.ZodString;
@@ -1196,6 +1272,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1196
1272
  pausedUntil?: string | null | undefined;
1197
1273
  backgroundTasks?: number | undefined;
1198
1274
  agentLatestVersion?: string | undefined;
1275
+ promptAuthor?: string | undefined;
1199
1276
  skipAgentPrompt?: boolean | undefined;
1200
1277
  branchHint?: string | undefined;
1201
1278
  branchPlan?: {
@@ -1247,6 +1324,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1247
1324
  pausedUntil?: unknown;
1248
1325
  backgroundTasks?: unknown;
1249
1326
  agentLatestVersion?: unknown;
1327
+ promptAuthor?: unknown;
1250
1328
  skipAgentPrompt?: unknown;
1251
1329
  branchHint?: unknown;
1252
1330
  branchPlan?: unknown;
@@ -1295,6 +1373,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1295
1373
  pausedUntil?: string | null | undefined;
1296
1374
  backgroundTasks?: number | undefined;
1297
1375
  agentLatestVersion?: string | undefined;
1376
+ promptAuthor?: string | undefined;
1298
1377
  skipAgentPrompt?: boolean | undefined;
1299
1378
  branchHint?: string | undefined;
1300
1379
  branchPlan?: {
@@ -1349,6 +1428,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1349
1428
  pausedUntil?: unknown;
1350
1429
  backgroundTasks?: unknown;
1351
1430
  agentLatestVersion?: unknown;
1431
+ promptAuthor?: unknown;
1352
1432
  skipAgentPrompt?: unknown;
1353
1433
  branchHint?: unknown;
1354
1434
  branchPlan?: unknown;
@@ -1380,10 +1460,12 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1380
1460
  mimeType: string;
1381
1461
  fileSize: number;
1382
1462
  }>, "many">>>;
1463
+ author: z.ZodCatch<z.ZodOptional<z.ZodString>>;
1383
1464
  }, "strip", z.ZodTypeAny, {
1384
1465
  text: string;
1385
1466
  sessionId: string;
1386
1467
  type: "session_message";
1468
+ author?: string | undefined;
1387
1469
  attachments?: {
1388
1470
  id: string;
1389
1471
  fileName: string;
@@ -1395,6 +1477,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1395
1477
  text: string;
1396
1478
  sessionId: string;
1397
1479
  type: "session_message";
1480
+ author?: unknown;
1398
1481
  attachments?: unknown;
1399
1482
  messageId?: unknown;
1400
1483
  }>, z.ZodObject<{
@@ -1435,6 +1518,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1435
1518
  notes?: string | undefined;
1436
1519
  }>, "many">>;
1437
1520
  text: z.ZodOptional<z.ZodString>;
1521
+ author: z.ZodCatch<z.ZodOptional<z.ZodString>>;
1438
1522
  type: z.ZodLiteral<"question_answer">;
1439
1523
  sessionId: z.ZodString;
1440
1524
  }, "strip", z.ZodTypeAny, {
@@ -1443,6 +1527,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1443
1527
  action: "answer" | "discuss";
1444
1528
  askId: string;
1445
1529
  text?: string | undefined;
1530
+ author?: string | undefined;
1446
1531
  answers?: {
1447
1532
  values: string[];
1448
1533
  questionId: string;
@@ -1455,6 +1540,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1455
1540
  action: "answer" | "discuss";
1456
1541
  askId: string;
1457
1542
  text?: string | undefined;
1543
+ author?: unknown;
1458
1544
  answers?: {
1459
1545
  values: string[];
1460
1546
  questionId: string;
package/dist/protocol.js CHANGED
@@ -3,6 +3,12 @@ import { AGENT_DB_VALUES, AGENT_VERSION_PATTERN } from './agent-registry.js';
3
3
  // Wire protocol with the DevBridge API (mirror of
4
4
  // packages/api/src/modules/dev-orchestration/dev-orchestration.schema.ts —
5
5
  // the API side is the source of truth).
6
+ /**
7
+ * The longest name the wire accepts for «who typed this». A display name from a
8
+ * profile, not a document — anything longer is not a name, and the field is a
9
+ * nicety: over the limit it is dropped and the message goes on without it.
10
+ */
11
+ export const MESSAGE_AUTHOR_MAX = 200;
6
12
  export const SessionDescriptorSchema = z.object({
7
13
  id: z.string().uuid(),
8
14
  kind: z.enum(['TICKET', 'CHAT']),
@@ -102,6 +108,18 @@ export const SessionDescriptorSchema = z.object({
102
108
  .regex(new RegExp(AGENT_VERSION_PATTERN))
103
109
  .optional()
104
110
  .catch(undefined),
111
+ /**
112
+ * Who handed over the first message — the person who created the session, as
113
+ * DevBridge shows them. Sent only when the project has «Tell the agent who is
114
+ * writing» switched on and the prompt is not empty; `composeInitialPrompt`
115
+ * puts the name on top of the whole first message (`message-author.ts`).
116
+ *
117
+ * Optional, and absent means «no name»: an older API never sends it, and a
118
+ * project with the setting off gets exactly the first message it always had.
119
+ * `.catch(undefined)` like its neighbours — a bad value costs the signature,
120
+ * never the descriptor, never the `hello_ack` it rides in (QA-100 MAJOR-1).
121
+ */
122
+ promptAuthor: z.string().max(MESSAGE_AUTHOR_MAX).optional().catch(undefined),
105
123
  workspace: z.object({
106
124
  id: z.string().uuid(),
107
125
  path: z.string(),
@@ -263,6 +281,14 @@ export const QuestionAnswerShape = {
263
281
  .max(4)
264
282
  .optional(),
265
283
  text: z.string().max(20_000).optional(),
284
+ /**
285
+ * Who typed `text` — sent only when the project has «Tell the agent who is
286
+ * writing» on. Signs the free text of a «discuss» reply (words a person typed
287
+ * into an open card are a message like any other) and the words rescued when
288
+ * the card turns out to be dead; a pick stays a pick. `.catch(undefined)`
289
+ * like the message author: a bad name costs the signature, not the answer.
290
+ */
291
+ author: z.string().max(MESSAGE_AUTHOR_MAX).optional().catch(undefined),
266
292
  };
267
293
  /** The `answer_question` command's arguments — the shape above on its own. */
268
294
  export const QuestionAnswerArgsSchema = z.object(QuestionAnswerShape);
@@ -295,6 +321,14 @@ export const DeliverMessageArgsSchema = z.object({
295
321
  messageId: z.string().min(1).max(120),
296
322
  text: z.string(),
297
323
  attachments: MessageAttachmentsSchema,
324
+ /**
325
+ * Who typed it, as DevBridge shows them — present only when the project has
326
+ * «Tell the agent who is writing» switched on. Goes on top of the composed
327
+ * prompt (`message-author.ts`), never into the feed echo. `.catch(undefined)`
328
+ * for the same reason attachments have it: a malformed name must cost the
329
+ * signature, never the sentence the person typed.
330
+ */
331
+ author: z.string().max(MESSAGE_AUTHOR_MAX).optional().catch(undefined),
298
332
  });
299
333
  export const GatewayFrameSchema = z.discriminatedUnion('type', [
300
334
  z.object({
@@ -334,6 +368,10 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
334
368
  // fetched with the runner's own token. `.catch` so a malformed entry costs
335
369
  // the attachments, never the message the user typed.
336
370
  attachments: MessageAttachmentsSchema,
371
+ // Who typed it — the same field `deliver_message` carries, on the legacy
372
+ // door too, so a project with the setting on is not silently unsigned on
373
+ // an API that still sends the frame.
374
+ author: z.string().max(MESSAGE_AUTHOR_MAX).optional().catch(undefined),
337
375
  }),
338
376
  z.object({
339
377
  type: z.literal('permission_answer'),
@@ -35,6 +35,7 @@ import { countRunningCommands, freshStallState, pickKillCandidate, readScopeProc
35
35
  import { machineReserveBytes } from './service-unit.js';
36
36
  import { sessionLimitsChangedEnough, sessionLimitsHeartbeatDue, SESSION_LIMITS_HEARTBEAT_MS, } from './session-limits.js';
37
37
  import { composeMessageWithAttachments, saveAttachments, } from './attachments.js';
38
+ import { signMessage } from './message-author.js';
38
39
  import { applyRewind, createCheckpoint, dropCheckpoints, listCheckpoints, MAX_BUSY_SESSIONS, previewRewind, pruneCheckpoints, } from './checkpoints.js';
39
40
  import { DeliverMessageArgsSchema, QuestionAnswerArgsSchema } from './protocol.js';
40
41
  import { availableModes, MODE_REFUSED_TEXT } from './adapters/types.js';
@@ -1828,7 +1829,7 @@ export class Supervisor {
1828
1829
  // command yet. Its outcome goes nowhere because nobody is listening —
1829
1830
  // which is the whole defect ticket #299 is about, and why the command
1830
1831
  // below exists.
1831
- this.acceptUserMessage(frame.sessionId, frame.text, frame.attachments, frame.messageId ?? undefined);
1832
+ this.acceptUserMessage(frame.sessionId, frame.text, frame.attachments, frame.messageId ?? undefined, frame.author);
1832
1833
  break;
1833
1834
  case 'permission_answer': {
1834
1835
  const running = this.sessions.get(frame.sessionId);
@@ -1914,6 +1915,16 @@ export class Supervisor {
1914
1915
  existing.stopRequested = true;
1915
1916
  existing.session?.stop('session_stopped');
1916
1917
  }
1918
+ // The descriptor also knows the feed better than this life does (#472).
1919
+ // The API writes lines of its own into a feed this runner numbers: the
1920
+ // refusal service puts «Continuing the session by itself» at the next
1921
+ // free seq and resumes a moment later, while this life is still going
1922
+ // down. The words that come with the resume are echoed by THIS life, and
1923
+ // its counter knew nothing of that line – the echo reused its number and
1924
+ // the API dropped it as a duplicate. Before the `await` below: the words
1925
+ // arrive right behind this frame. A no-op whenever this runner wrote the
1926
+ // last row itself.
1927
+ existing.journal.ensureSeqAbove(descriptor.lastSeq);
1917
1928
  // The descriptor is newer than what this session was built from, so its
1918
1929
  // clock is too (QA-149 MAJOR-1). Applied on the way out of every early
1919
1930
  // return, not just the reconnect one — a descriptor re-sent for any
@@ -2021,7 +2032,7 @@ export class Supervisor {
2021
2032
  if (orphaned) {
2022
2033
  this.orphanMessages.delete(descriptor.id);
2023
2034
  for (const message of orphaned) {
2024
- this.acceptUserMessage(descriptor.id, message.text, message.attachments, message.messageId);
2035
+ this.acceptUserMessage(descriptor.id, message.text, message.attachments, message.messageId, message.author);
2025
2036
  }
2026
2037
  }
2027
2038
  try {
@@ -2935,9 +2946,18 @@ export class Supervisor {
2935
2946
  }
2936
2947
  // Keep the journal while offline: the recorded terminal status is what
2937
2948
  // reconcile replays after the next reconnect (QA-96 F1).
2949
+ //
2950
+ // …and while a resume is queued behind this life (#472). The next life
2951
+ // opens this same journal, and two things it needs live only here: the
2952
+ // words parked for it (`acceptUserMessage` holds a message that reached a
2953
+ // life already on its way out) and the seq counter. Deleted, the resume
2954
+ // started from an empty file – the platform's «continue» after a refusal
2955
+ // was gone, and «Session resumed» reused a seq the old life had already
2956
+ // sent, so the API dropped it. The session sat idle with nothing on screen.
2938
2957
  if (this.ws.connected &&
2939
2958
  running.journal.unacked().length === 0 &&
2940
- isTerminal(running.lastReported)) {
2959
+ isTerminal(running.lastReported) &&
2960
+ !running.pendingRestart) {
2941
2961
  this.journals.closeAndDelete(descriptor.id);
2942
2962
  }
2943
2963
  // The map just lost an entry, and this is the one place where that happens
@@ -4293,11 +4313,18 @@ export class Supervisor {
4293
4313
  // own events reach the feed a microtask later, so the user's words still
4294
4314
  // land before the agent's reaction — and on a miss we can hand the text to
4295
4315
  // the ordinary delivery path, which does the echo itself.
4316
+ //
4317
+ // The free text of a «discuss» reply is a person's message like any other,
4318
+ // so it is signed for the agent the way `acceptUserMessage` signs one
4319
+ // (`message-author.ts`) — and only for the agent: the echo below stays the
4320
+ // words as typed. A pick (`answer`) is structured and gets no line.
4296
4321
  const accepted = running.session?.answerQuestion({
4297
4322
  askId: frame.askId,
4298
4323
  action: frame.action,
4299
4324
  ...(frame.answers ? { answers: frame.answers } : {}),
4300
- ...(frame.text !== undefined ? { text: frame.text } : {}),
4325
+ ...(frame.text !== undefined
4326
+ ? { text: frame.action === 'discuss' ? signMessage(frame.text, frame.author) : frame.text }
4327
+ : {}),
4301
4328
  });
4302
4329
  if (accepted) {
4303
4330
  running.answeredAsks.add(frame.askId);
@@ -4372,7 +4399,7 @@ export class Supervisor {
4372
4399
  this.sendEvent(running, 'system_note', {
4373
4400
  text: 'That question is no longer open — sending your reply as an ordinary message instead.',
4374
4401
  });
4375
- this.acceptUserMessage(frame.sessionId, rescued);
4402
+ this.acceptUserMessage(frame.sessionId, rescued, undefined, undefined, frame.author);
4376
4403
  return 'not_open';
4377
4404
  }
4378
4405
  // Nothing to deliver — but the API optimistically flipped the session to
@@ -4395,7 +4422,14 @@ export class Supervisor {
4395
4422
  * thrown away, because «I wrote it into a socket» was never an answer to
4396
4423
  * «did the runner get it».
4397
4424
  */
4398
- acceptUserMessage(sessionId, text, attachments, messageId) {
4425
+ acceptUserMessage(sessionId, text, attachments, messageId,
4426
+ /**
4427
+ * Who typed it, as DevBridge shows them — sent only when the project has
4428
+ * «Tell the agent who is writing» on. Goes on top of the COMPOSED prompt
4429
+ * (`message-author.ts`), the way attachment paths do; the feed echo below
4430
+ * stays the bare text, exactly as the person wrote it.
4431
+ */
4432
+ author) {
4399
4433
  const running = this.sessions.get(sessionId);
4400
4434
  if (!running) {
4401
4435
  // The API believes this session lives here but the runner lost track of
@@ -4417,6 +4451,7 @@ export class Supervisor {
4417
4451
  text,
4418
4452
  ...(attachments?.length ? { attachments } : {}),
4419
4453
  ...(messageId ? { messageId } : {}),
4454
+ ...(author ? { author } : {}),
4420
4455
  });
4421
4456
  this.orphanMessages.set(sessionId, queued.slice(-Supervisor.ORPHAN_MESSAGE_CAP));
4422
4457
  this.ws.send({ type: 'session_unknown', sessionId });
@@ -4483,17 +4518,17 @@ export class Supervisor {
4483
4518
  // check — the outbox flushes on reconnect, and the git service posts its
4484
4519
  // own conflict tasks. Held here rather than delivered, so «на паузе»
4485
4520
  // means the same thing whichever door the words came through.
4486
- this.queueMessage(running, running.journal.appendPending(text, attachments, originSeq));
4521
+ this.queueMessage(running, running.journal.appendPending(text, attachments, originSeq, author));
4487
4522
  return 'accepted';
4488
4523
  }
4489
4524
  this.enqueueDelivery(running, async () => {
4490
- const composed = await this.materializeAttachments(running, text, attachments);
4525
+ const composed = signMessage(await this.materializeAttachments(running, text, attachments), author);
4491
4526
  if (this.isStale(running)) {
4492
4527
  // The session ended while the files downloaded. Park the message on
4493
4528
  // disk rather than dropping it — «Продолжить» carries it to the agent.
4494
4529
  // Announced like every other queueing decision: a bubble that is
4495
4530
  // waiting must say so, whichever door it came through.
4496
- const parked = running.journal.appendPending(text, attachments, originSeq);
4531
+ const parked = running.journal.appendPending(text, attachments, originSeq, author);
4497
4532
  if (parked.seq !== undefined) {
4498
4533
  this.sendEvent(running, 'message_queued', { targetSeq: parked.seq });
4499
4534
  }
@@ -5077,7 +5112,10 @@ export class Supervisor {
5077
5112
  const parts = [];
5078
5113
  try {
5079
5114
  for (const record of pending) {
5080
- parts.push(await this.materializeAttachments(running, record.text, record.attachments));
5115
+ // Each part under its own name: two people who wrote while the
5116
+ // session was held go to the agent together, and the agent must
5117
+ // still be able to tell which words are whose.
5118
+ parts.push(signMessage(await this.materializeAttachments(running, record.text, record.attachments), record.author));
5081
5119
  }
5082
5120
  }
5083
5121
  catch (error) {
@@ -6507,7 +6545,7 @@ export class Supervisor {
6507
6545
  error: parsed.error.issues[0]?.message ?? 'malformed message',
6508
6546
  });
6509
6547
  }
6510
- const outcome = this.acceptUserMessage(sessionId, parsed.data.text, parsed.data.attachments, parsed.data.messageId);
6548
+ const outcome = this.acceptUserMessage(sessionId, parsed.data.text, parsed.data.attachments, parsed.data.messageId, parsed.data.author);
6511
6549
  // `ok` is about the RELAY, like the answer above: `unknown_session` is
6512
6550
  // the one case the API can fix by asking again after it has re-sent
6513
6551
  // the descriptor, so it is the one case reported as a failure.
@@ -8219,6 +8257,15 @@ export function carryOnMessage(refusedWhole) {
8219
8257
  * prompt beside `CLAUDE.md` — which is read last and therefore overrides ours.
8220
8258
  */
8221
8259
  export function composeInitialPrompt(descriptor) {
8260
+ const body = composeInitialBody(descriptor);
8261
+ // Signed like every later message (`message-author.ts`): the API names the
8262
+ // creator only when the project has «Tell the agent who is writing» on. The
8263
+ // name goes above the assignment too — handing a ticket over is that
8264
+ // person's act. An empty first message stays empty: there is nothing to sign
8265
+ // and the agent is meant to boot and wait.
8266
+ return body === '' ? body : signMessage(body, descriptor.promptAuthor);
8267
+ }
8268
+ function composeInitialBody(descriptor) {
8222
8269
  // A free chat may carry tickets as CONTEXT. Assigning them would put an agent
8223
8270
  // nobody asked onto somebody else's ticket (session 13).
8224
8271
  if (descriptor.kind === 'CHAT' || descriptor.tickets.length === 0)
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.69.1";
1
+ export declare const RUNNER_VERSION = "0.70.1";
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.69.1';
2
+ export const RUNNER_VERSION = '0.70.1';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge4dev/runner",
3
- "version": "0.69.1",
3
+ "version": "0.70.1",
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",