@songsid/agend 2.2.0-beta.6 → 2.2.0-beta.8

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.
Files changed (134) hide show
  1. package/dist/agent-cli-instructions.md +6 -2
  2. package/dist/agent-cli.js +9 -5
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-whats-new.js +9 -3
  5. package/dist/agent-whats-new.js.map +1 -1
  6. package/dist/backend/kiro.d.ts +9 -0
  7. package/dist/backend/kiro.js +23 -0
  8. package/dist/backend/kiro.js.map +1 -1
  9. package/dist/backend/muse.d.ts +26 -0
  10. package/dist/backend/muse.js +131 -0
  11. package/dist/backend/muse.js.map +1 -1
  12. package/dist/channel/adapters/discord.js +12 -1
  13. package/dist/channel/adapters/discord.js.map +1 -1
  14. package/dist/channel/adapters/telegram.js +6 -0
  15. package/dist/channel/adapters/telegram.js.map +1 -1
  16. package/dist/channel/mcp-tools.js +3 -3
  17. package/dist/channel/mcp-tools.js.map +1 -1
  18. package/dist/channel/types.d.ts +3 -0
  19. package/dist/config-validator.js +3 -0
  20. package/dist/config-validator.js.map +1 -1
  21. package/dist/cross-instance-envelope.js +2 -0
  22. package/dist/cross-instance-envelope.js.map +1 -1
  23. package/dist/daemon.d.ts +8 -0
  24. package/dist/daemon.js +126 -54
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/delivery-outbox.d.ts +27 -0
  27. package/dist/delivery-outbox.js +175 -6
  28. package/dist/delivery-outbox.js.map +1 -1
  29. package/dist/fleet-manager.d.ts +40 -1
  30. package/dist/fleet-manager.js +248 -40
  31. package/dist/fleet-manager.js.map +1 -1
  32. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  33. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  34. package/dist/human-delivery-receipts.d.ts +83 -0
  35. package/dist/human-delivery-receipts.js +194 -0
  36. package/dist/human-delivery-receipts.js.map +1 -0
  37. package/dist/instance-config-impact.js +1 -0
  38. package/dist/instance-config-impact.js.map +1 -1
  39. package/dist/instructions.d.ts +1 -1
  40. package/dist/instructions.js +1 -1
  41. package/dist/instructions.js.map +1 -1
  42. package/dist/locale.js +8 -0
  43. package/dist/locale.js.map +1 -1
  44. package/dist/log-service.d.ts +99 -0
  45. package/dist/log-service.js +235 -0
  46. package/dist/log-service.js.map +1 -0
  47. package/dist/log-steps.d.ts +143 -0
  48. package/dist/log-steps.js +646 -0
  49. package/dist/log-steps.js.map +1 -0
  50. package/dist/outbound-handlers.d.ts +3 -0
  51. package/dist/outbound-handlers.js +20 -4
  52. package/dist/outbound-handlers.js.map +1 -1
  53. package/dist/outbound-schemas.d.ts +15 -13
  54. package/dist/outbound-schemas.js +5 -0
  55. package/dist/outbound-schemas.js.map +1 -1
  56. package/dist/public-link-progress.d.ts +1 -1
  57. package/dist/public-link-progress.js +8 -2
  58. package/dist/public-link-progress.js.map +1 -1
  59. package/dist/public-preview.d.ts +2 -1
  60. package/dist/public-preview.js +8 -5
  61. package/dist/public-preview.js.map +1 -1
  62. package/dist/public-web-gateway.js +1 -1
  63. package/dist/public-web-gateway.js.map +1 -1
  64. package/dist/public-web-link.d.ts +4 -1
  65. package/dist/public-web-link.js +42 -13
  66. package/dist/public-web-link.js.map +1 -1
  67. package/dist/reply-buttons-controller.d.ts +3 -0
  68. package/dist/reply-buttons-controller.js +3 -2
  69. package/dist/reply-buttons-controller.js.map +1 -1
  70. package/dist/settings-api.js +4 -0
  71. package/dist/settings-api.js.map +1 -1
  72. package/dist/settings-change.js +2 -0
  73. package/dist/settings-change.js.map +1 -1
  74. package/dist/settings-effect.js +2 -0
  75. package/dist/settings-effect.js.map +1 -1
  76. package/dist/tool-progress.js +3 -2
  77. package/dist/tool-progress.js.map +1 -1
  78. package/dist/topic-commands.d.ts +3 -2
  79. package/dist/topic-commands.js +14 -4
  80. package/dist/topic-commands.js.map +1 -1
  81. package/dist/transcript-jsonl.d.ts +80 -3
  82. package/dist/transcript-jsonl.js +147 -10
  83. package/dist/transcript-jsonl.js.map +1 -1
  84. package/dist/transcript-monitor.d.ts +11 -0
  85. package/dist/transcript-monitor.js +61 -13
  86. package/dist/transcript-monitor.js.map +1 -1
  87. package/dist/transcript-sources.d.ts +24 -0
  88. package/dist/transcript-sources.js +205 -55
  89. package/dist/transcript-sources.js.map +1 -1
  90. package/dist/tunnel/cloudflared.d.ts +7 -15
  91. package/dist/tunnel/cloudflared.js +36 -1
  92. package/dist/tunnel/cloudflared.js.map +1 -1
  93. package/dist/tunnel/types.d.ts +5 -1
  94. package/dist/tunnel/types.js.map +1 -1
  95. package/dist/types.d.ts +6 -0
  96. package/dist/ui/chat-strings.js +8 -2
  97. package/dist/ui/chat-thread.js +24 -4
  98. package/dist/ui/chat-time.js +80 -0
  99. package/dist/ui/panel-log.js +388 -0
  100. package/dist/ui/panel-settings.js +6 -1
  101. package/dist/ui/preview-panel.js +7 -1
  102. package/dist/ui/preview.js +5 -1
  103. package/dist/ui/settings-strings.js +2 -0
  104. package/dist/ui/shared/app-i18n.js +2 -2
  105. package/dist/ui/shared/app-route.js +6 -1
  106. package/dist/ui/shared/app-shell.js +3 -3
  107. package/dist/ui/shared/app-stream.js +2 -1
  108. package/dist/ui/shared/app.css +44 -6
  109. package/dist/ui/shared/app.js +5 -0
  110. package/dist/ui/shared/fonts/README.md +1 -1
  111. package/dist/ui/shared/instance-nav.js +9 -6
  112. package/dist/ui/shared/instance-switch.js +4 -4
  113. package/dist/ui/shared/tokens.css +1 -1
  114. package/dist/web-api.d.ts +5 -0
  115. package/dist/web-api.js +49 -1
  116. package/dist/web-api.js.map +1 -1
  117. package/dist/web-auth.js +2 -1
  118. package/dist/web-auth.js.map +1 -1
  119. package/dist/web-commands.d.ts +2 -2
  120. package/dist/web-commands.js +3 -2
  121. package/dist/web-commands.js.map +1 -1
  122. package/dist/web-reply-files.d.ts +45 -0
  123. package/dist/web-reply-files.js +223 -0
  124. package/dist/web-reply-files.js.map +1 -0
  125. package/dist/web-settings-patch.d.ts +3 -1
  126. package/dist/web-settings-patch.js +4 -1
  127. package/dist/web-settings-patch.js.map +1 -1
  128. package/dist/web-shell-routes.d.ts +4 -0
  129. package/dist/web-shell-routes.js +9 -0
  130. package/dist/web-shell-routes.js.map +1 -1
  131. package/dist/web-upload.d.ts +17 -4
  132. package/dist/web-upload.js +64 -13
  133. package/dist/web-upload.js.map +1 -1
  134. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -33,6 +33,7 @@ import { HangDetector } from "./hang-detector.js";
33
33
  import { writeSecretFile } from "./secret-file.js";
34
34
  import { PaneWriteLock } from "./pane-write-lock.js";
35
35
  import { buildFleetInstructions } from "./instructions.js";
36
+ import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
36
37
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
37
38
  import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
38
39
  import { TurnReplyGuard } from "./turn-reply-guard.js";
@@ -1088,6 +1089,8 @@ export class Daemon extends EventEmitter {
1088
1089
  ipcServer = null;
1089
1090
  messageBus;
1090
1091
  transcriptMonitor = null;
1092
+ /** #1218: the transcript this instance's monitor follows now (the Log tab reads it on demand; nothing runs for it here). */
1093
+ logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
1091
1094
  guardian = null;
1092
1095
  adapter = null;
1093
1096
  pendingIpcRequests = new Map();
@@ -2003,6 +2006,7 @@ export class Daemon extends EventEmitter {
2003
2006
  }
2004
2007
  }
2005
2008
  deferDurableDelivery(meta, reason) {
2009
+ this.finishHumanReceipt(meta, { reached: false });
2006
2010
  const delivery = this.durableDeliveryAttempt(meta);
2007
2011
  if (delivery === null || delivery === false)
2008
2012
  return;
@@ -6322,11 +6326,7 @@ export class Daemon extends EventEmitter {
6322
6326
  * is a durable delivery) must LEAD the persisted user message — restart reconciliation recognises a delivery only by a
6323
6327
  * marker at the start of the body (#1197) — so the banner goes right after it, not in front of it.
6324
6328
  */
6325
- static steerEnvelope(inbound) {
6326
- const banner = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n";
6327
- const marker = inbound.match(/^\[agend-delivery-id:[^\]\r\n]+\]\r?\n/);
6328
- return marker ? marker[0] + banner + inbound.slice(marker[0].length) : banner + inbound;
6329
- }
6329
+ static steerEnvelope(inbound) { return steerInboundEnvelope(inbound); }
6330
6330
  /**
6331
6331
  * The one place an inbound message grows its metadata wrapper ([user:]/
6332
6332
  * [from:] prefix, pending reactions, handoff metadata, reply instructions).
@@ -6337,35 +6337,65 @@ export class Daemon extends EventEmitter {
6337
6337
  /** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
6338
6338
  agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
6339
6339
  formatInboundMessage(content, meta) {
6340
- const user = meta.user || "unknown";
6341
- const fromInstance = meta.from_instance;
6342
- let formatted;
6343
- if (fromInstance) {
6344
- formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6340
+ if (!meta.from_instance)
6341
+ return formatHumanInboundMessage(content, meta, this.agentMode());
6342
+ let formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6343
+ if (meta.reply_to_text)
6344
+ formatted += '\n(reply_to: "' + meta.reply_to_text + '")';
6345
+ if (meta.delivery_id)
6346
+ formatted = "[agend-delivery-id:" + meta.delivery_id + "]\n" + formatted;
6347
+ return formatted;
6348
+ }
6349
+ /** Pure cached owner: no pane/process query on ingress. */
6350
+ captureHumanReceiptOwner() {
6351
+ try {
6352
+ if (this.deliveryWritesStopping || this.spawning || !this.tmux)
6353
+ return null;
6354
+ const o = this.interactionOwner();
6355
+ const window = this.tmux.getWindowId();
6356
+ if (!window || !o.bootId || ![o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch, this.deliveryEpoch]
6357
+ .every(n => Number.isSafeInteger(n) && n >= 0))
6358
+ return null;
6359
+ return JSON.stringify([o.bootId, o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch,
6360
+ this.deliveryEpoch, humanOwnerObjectId(this.tmux), window]);
6345
6361
  }
6346
- else {
6347
- const via = meta.source ? ` via ${meta.source}` : "";
6348
- const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
6349
- formatted = `[user:${user}${via}${idTag}] ${content}`;
6350
- // Reactions queued since the last real message (#432). One leading line of
6351
- // context, present only when something is pending — a reaction no longer
6352
- // costs a turn of its own.
6353
- if (meta.pending_reactions) {
6354
- formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
6355
- }
6356
- formatted += renderHandoffMetadata(meta);
6357
- // #1650: a CLI-mode agent has no reply tool — it answers with the agend-agent command.
6358
- formatted += this.agentMode() === "cli"
6359
- ? '\n(Reply with agend-agent reply "…" — do NOT respond with direct text)'
6360
- : "\n(Reply using the reply tool — do NOT respond with direct text)";
6361
- }
6362
- if (meta.reply_to_text) {
6363
- formatted += `\n(reply_to: "${meta.reply_to_text}")`;
6364
- }
6365
- if (meta.delivery_id) {
6366
- formatted = `[agend-delivery-id:${meta.delivery_id}]\n${formatted}`;
6362
+ catch {
6363
+ return null;
6364
+ }
6365
+ }
6366
+ /** The same formatter/sanitizer used by the actual write; snapshot only. */
6367
+ humanReceiptWire(content, meta, kind) {
6368
+ let wire = kind === "raw_paste" ? content
6369
+ : !meta.from_instance && content.startsWith("/raw ") && kind === "fleet_inbound"
6370
+ ? content.slice(5) : this.formatInboundMessage(content, meta);
6371
+ if (kind === "steer")
6372
+ wire = Daemon.steerEnvelope(wire);
6373
+ else if (kind === "btw")
6374
+ wire = BTW_INBOUND_PREFIX + wire;
6375
+ return prepareDeliveryWire(wire, kind === "raw_paste");
6376
+ }
6377
+ humanReceiptAttempt(meta, wire) {
6378
+ const id = meta.human_receipt_id;
6379
+ const daemonOwner = meta.human_daemon_owner;
6380
+ if (!id || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(id) || meta.verification_id !== "hmsg-" + id
6381
+ || !daemonOwner || !meta.human_owner_key || !/^[a-f0-9]{64}$/.test(meta.human_wire_sha256 ?? "")
6382
+ || humanBodyHash(prepareDeliveryWire(wire)) !== meta.human_wire_sha256)
6383
+ return undefined;
6384
+ return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
6385
+ ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
6386
+ }
6387
+ finishHumanReceipt(meta, verdict) {
6388
+ if (!meta.human_receipt_id || !meta.human_owner_key)
6389
+ return;
6390
+ const attempt = verdict.humanReceipt;
6391
+ const proof = attempt?.current() && !["unverified", "output-edge", "unverifiable"].includes(verdict.proof ?? "")
6392
+ ? attempt.proof ?? null : null;
6393
+ try {
6394
+ this.emit("human_receipt_result", { receipt_id: meta.human_receipt_id, owner_key: meta.human_owner_key, proof });
6395
+ }
6396
+ catch (err) {
6397
+ this.logger.error({ err }, "Could not publish human delivery evidence");
6367
6398
  }
6368
- return formatted;
6369
6399
  }
6370
6400
  /**
6371
6401
  * Whether ONE delivery reached a verdict, carried by that delivery.
@@ -6458,12 +6488,13 @@ export class Daemon extends EventEmitter {
6458
6488
  this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
6459
6489
  return;
6460
6490
  }
6461
- const verdict = { reached: false };
6491
+ const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6462
6492
  try {
6463
6493
  if (await this.deliverMessage(formatted, status, {
6464
6494
  steer: true,
6465
6495
  deliveryEpoch,
6466
6496
  submissionId: meta.message_id,
6497
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
6467
6498
  verdict,
6468
6499
  durableAttempt: durableAttempt || undefined,
6469
6500
  })) {
@@ -6494,6 +6525,9 @@ export class Daemon extends EventEmitter {
6494
6525
  this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
6495
6526
  throw err;
6496
6527
  }
6528
+ finally {
6529
+ this.finishHumanReceipt(meta, verdict);
6530
+ }
6497
6531
  }).catch(err => {
6498
6532
  this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
6499
6533
  this.logger.warn({ err: err.message }, "steer delivery error");
@@ -6509,24 +6543,37 @@ export class Daemon extends EventEmitter {
6509
6543
  * so channel delivery intentionally uses a normal wrapped inbound instead.
6510
6544
  */
6511
6545
  btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
6512
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6546
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6547
+ this.finishHumanReceipt(meta, { reached: false });
6513
6548
  return;
6549
+ }
6514
6550
  this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
6515
6551
  this.pendingWork.recordInbound();
6516
6552
  this.recordRecentUserMessage(content, meta);
6517
- const formatted = "[BTW — side question from the user. Answer it separately without changing or interrupting the CURRENT work.]\n"
6518
- + this.formatInboundMessage(content, meta);
6553
+ const formatted = BTW_INBOUND_PREFIX + this.formatInboundMessage(content, meta);
6519
6554
  const status = channelStatus(meta);
6520
6555
  this.steerLock = this.steerLock.then(async () => {
6521
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6556
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6557
+ this.finishHumanReceipt(meta, { reached: false });
6522
6558
  return;
6559
+ }
6523
6560
  await this.wake();
6524
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6561
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6562
+ this.finishHumanReceipt(meta, { reached: false });
6525
6563
  return;
6526
- if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id })) {
6527
- this.markTurnStarted(meta, formatted, deliveryEpoch);
6564
+ }
6565
+ const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6566
+ try {
6567
+ if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
6568
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
6569
+ this.markTurnStarted(meta, formatted, deliveryEpoch);
6570
+ }
6571
+ }
6572
+ finally {
6573
+ this.finishHumanReceipt(meta, verdict);
6528
6574
  }
6529
6575
  }).catch(err => {
6576
+ this.finishHumanReceipt(meta, { reached: false });
6530
6577
  this.logger.warn({ err: err.message }, "btw delivery error");
6531
6578
  });
6532
6579
  }
@@ -6640,7 +6687,7 @@ export class Daemon extends EventEmitter {
6640
6687
  // A fresh delivery begins a fresh turn — its bubble must not inherit
6641
6688
  // the previous turn's tool list.
6642
6689
  this.resetToolProgress();
6643
- verdict = { reached: false };
6690
+ verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6644
6691
  const durableAttempt = this.durableDeliveryAttempt(meta, content);
6645
6692
  if (durableAttempt === false) {
6646
6693
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
@@ -6650,6 +6697,7 @@ export class Daemon extends EventEmitter {
6650
6697
  if (await this.deliverMessage(formatted, status, {
6651
6698
  deliveryEpoch,
6652
6699
  submissionId: meta.message_id,
6700
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
6653
6701
  verdict,
6654
6702
  durableAttempt: durableAttempt || undefined,
6655
6703
  })) {
@@ -6697,6 +6745,7 @@ export class Daemon extends EventEmitter {
6697
6745
  this.logger.warn({ err: err.message }, "Durable pane delivery deferred after exception");
6698
6746
  }
6699
6747
  finally {
6748
+ this.finishHumanReceipt(meta, verdict);
6700
6749
  this.pasteQueueDepth--;
6701
6750
  }
6702
6751
  }).catch(err => {
@@ -6733,12 +6782,7 @@ export class Daemon extends EventEmitter {
6733
6782
  && !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
6734
6783
  if (cancelled() || this.stormWindow?.isStopped())
6735
6784
  return false;
6736
- // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
6737
- const fenceCount = (formatted.match(/```/g) || []).length;
6738
- if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
6739
- // Odd number of fences = unclosed. Remove all code fences from the message.
6740
- formatted = formatted.replace(/```/g, "");
6741
- }
6785
+ formatted = prepareDeliveryWire(formatted, opts?.preserveExactBytes);
6742
6786
  // Direct paths such as /steer, /btw, /raw and schedules do not pass the
6743
6787
  // FleetManager idle gate. Hold them at the final common pane-write path so
6744
6788
  // a tmux storm cannot force any text into a half-booted CLI.
@@ -6911,7 +6955,7 @@ export class Daemon extends EventEmitter {
6911
6955
  return stale();
6912
6956
  if (residue !== "clear")
6913
6957
  return this.failDelivery(verdict, status, "input-residue", residue);
6914
- const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
6958
+ const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent, opts?.humanVerificationId);
6915
6959
  // #1690: still under the lock — whatever waits on it next must already see what this write started.
6916
6960
  if (verdict.paneWriteStarted)
6917
6961
  opts?.onPaneWritten?.();
@@ -7706,7 +7750,7 @@ export class Daemon extends EventEmitter {
7706
7750
  return false;
7707
7751
  const row = norm(shown);
7708
7752
  // The marker line holds nothing but the marker: the row is exactly it.
7709
- if (/^\[agend-delivery-id:[^\]\s]+\]$/.test(first))
7753
+ if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
7710
7754
  return row === first;
7711
7755
  return row === first || row.startsWith(`${first} `);
7712
7756
  }
@@ -8248,6 +8292,8 @@ export class Daemon extends EventEmitter {
8248
8292
  if (!current())
8249
8293
  return false;
8250
8294
  if (evidence === "submitted") {
8295
+ if (pending.signature.unique)
8296
+ pending.signature.humanPositive?.("unique-id-pane");
8251
8297
  if (status)
8252
8298
  this.emit("message_confirmed", status);
8253
8299
  return true;
@@ -8283,6 +8329,8 @@ export class Daemon extends EventEmitter {
8283
8329
  if (retry === "cancelled")
8284
8330
  return false;
8285
8331
  if (retry === "submitted") {
8332
+ if (pending.signature.unique)
8333
+ pending.signature.humanPositive?.("unique-id-pane");
8286
8334
  if (status)
8287
8335
  this.emit("message_confirmed", status);
8288
8336
  return true;
@@ -8423,9 +8471,24 @@ export class Daemon extends EventEmitter {
8423
8471
  verdict = { reached: false }, durableAttempt, steer = false,
8424
8472
  // #829: the caller's fence, asked once more after the last await before the
8425
8473
  // first side effect. Stale means unwritten: false, with no failure verdict.
8426
- stillCurrent) {
8427
- const signature = this.submissionSignature(formatted, submissionId);
8474
+ stillCurrent, humanVerificationId) {
8475
+ const signature = this.submissionSignature(formatted, humanVerificationId ?? submissionId, humanVerificationId !== undefined);
8428
8476
  const rawPaste = durableAttempt?.submissionMode === "raw_paste";
8477
+ const humanAttempt = verdict.humanReceipt;
8478
+ const humanMode = rawPaste ? "raw_paste" : steer ? "steer"
8479
+ : handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
8480
+ if (humanAttempt && signature.unique && signature.value === "agend-human-id:" + humanAttempt.verificationId
8481
+ && humanBodyHash(formatted) === humanAttempt.expectedWireHash) {
8482
+ signature.humanPositive = tag => {
8483
+ if (!humanAttempt.current() || (stillCurrent && !stillCurrent()))
8484
+ return;
8485
+ humanAttempt.proof = {
8486
+ receipt_id: humanAttempt.receiptId, verification_id: humanAttempt.verificationId,
8487
+ wire_sha256: humanBodyHash(formatted), owner_key: humanAttempt.ownerKey,
8488
+ mode: humanMode, tag,
8489
+ };
8490
+ };
8491
+ }
8429
8492
  let windowId = initialWindowId;
8430
8493
  // The spawn this write belongs to: acceptance evidence is only read while it is still the current one (#1197).
8431
8494
  const spawnAtWrite = this.spawnGeneration;
@@ -8571,6 +8634,7 @@ export class Daemon extends EventEmitter {
8571
8634
  // positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
8572
8635
  // paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
8573
8636
  const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
8637
+ let humanComposerOwned = false;
8574
8638
  let steerEnterDeadline = Infinity;
8575
8639
  if (steerComposerProof) {
8576
8640
  const deadline = performance.now() + STEER_PASTE_VISIBLE_MS;
@@ -8594,6 +8658,8 @@ export class Daemon extends EventEmitter {
8594
8658
  return this.failDelivery(verdict, status, "steer-paste", before === null ? "box-unread" : `box-still-${before.mode}`);
8595
8659
  if (!this.steerPasteIsOurs(before.pane, formatted))
8596
8660
  return this.failDelivery(verdict, status, "steer-paste", "box-not-this-paste");
8661
+ const shown = this.backend?.readSteerComposerText?.(before.pane)?.replace(/\s+/g, " ").trim();
8662
+ humanComposerOwned = !!humanAttempt && shown === "[agend-human-id:" + humanAttempt.verificationId + "]";
8597
8663
  // …and so is the Enter: the key is sent only while the reading is still within it.
8598
8664
  steerEnterDeadline = deadline;
8599
8665
  }
@@ -8665,6 +8731,9 @@ export class Daemon extends EventEmitter {
8665
8731
  if (after === "steer" || after === "queue" || after === "idle") {
8666
8732
  const probe = await withinMs(this.probeBlockingDialog(), deadline - performance.now());
8667
8733
  if (probe?.state === "clear" && performance.now() <= deadline && steerCurrent() && this.getWindowId() === windowId) {
8734
+ if (humanComposerOwned)
8735
+ signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
8736
+ : after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
8668
8737
  verdict.phase = "steer-accepted";
8669
8738
  verdict.proof = `steer-composer-${after}`;
8670
8739
  if (status)
@@ -9251,8 +9320,11 @@ export class Daemon extends EventEmitter {
9251
9320
  // the input row is proof in itself — which also means a momentary
9252
9321
  // failure to read the pane BEFORE pasting cannot turn a delivered
9253
9322
  // message into a re-paste.
9254
- if (signature.unique && after.payload > 0)
9323
+ if (signature.unique && after.payload > 0) {
9324
+ if (after.inputReadable)
9325
+ signature.humanPositive?.("unique-id-pane");
9255
9326
  return this.submittedProof();
9327
+ }
9256
9328
  if (unattributedResidue)
9257
9329
  return "unverifiable";
9258
9330
  // 3. Otherwise the evidence must be NEW relative to the pane as it was
@@ -9389,7 +9461,7 @@ export class Daemon extends EventEmitter {
9389
9461
  * paste that never landed. System pastes carry no envelope and fall back to
9390
9462
  * the body signature, which the baseline comparison still ties to this paste.
9391
9463
  */
9392
- submissionSignature(formatted, trustedId) {
9464
+ submissionSignature(formatted, trustedId, verification = false) {
9393
9465
  // The id must come from the message's own metadata, never from scanning the
9394
9466
  // rendered text: agents and users discuss message ids in the body all the
9395
9467
  // time ("check message_id: abc"), and a body-scanned value would match an
@@ -9398,7 +9470,7 @@ export class Daemon extends EventEmitter {
9398
9470
  // the handoff metadata identifies THIS delivery.
9399
9471
  const id = trustedId?.trim();
9400
9472
  if (id)
9401
- return { value: `message_id:${id}`, unique: true };
9473
+ return { value: (verification ? "agend-human-id:" : "message_id:") + id, unique: true };
9402
9474
  return { value: pastedTextSignature(formatted), unique: false };
9403
9475
  }
9404
9476
  /**