instar 1.3.1639 → 1.3.1640

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 (109) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +18 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/AppConversationStore.d.ts +39 -0
  5. package/dist/core/AppConversationStore.d.ts.map +1 -1
  6. package/dist/core/AppConversationStore.js +9 -0
  7. package/dist/core/AppConversationStore.js.map +1 -1
  8. package/dist/core/AppConversationsReplicatedStore.js +1 -1
  9. package/dist/core/AppConversationsReplicatedStore.js.map +1 -1
  10. package/dist/core/AppReplyRelayInstaller.d.ts.map +1 -1
  11. package/dist/core/AppReplyRelayInstaller.js +2 -0
  12. package/dist/core/AppReplyRelayInstaller.js.map +1 -1
  13. package/dist/core/CodexLifecycleProductionComposition.d.ts +1 -1
  14. package/dist/core/CodexLifecycleProductionComposition.d.ts.map +1 -1
  15. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  16. package/dist/core/PostUpdateMigrator.js +9 -1
  17. package/dist/core/PostUpdateMigrator.js.map +1 -1
  18. package/dist/core/SlackReplyRelayInstaller.d.ts.map +1 -1
  19. package/dist/core/SlackReplyRelayInstaller.js +2 -0
  20. package/dist/core/SlackReplyRelayInstaller.js.map +1 -1
  21. package/dist/core/types.d.ts +16 -0
  22. package/dist/core/types.d.ts.map +1 -1
  23. package/dist/core/types.js.map +1 -1
  24. package/dist/data/standards-guard-index.json +1 -1
  25. package/dist/data/standards-guard-index.meta.json +2 -2
  26. package/dist/data/standards-registry.meta.json +1 -1
  27. package/dist/feedback-factory/execute/ConfinedRunner.d.ts +4 -1
  28. package/dist/feedback-factory/execute/ConfinedRunner.d.ts.map +1 -1
  29. package/dist/feedback-factory/execute/ConfinedRunner.js +40 -8
  30. package/dist/feedback-factory/execute/ConfinedRunner.js.map +1 -1
  31. package/dist/feedback-factory/execute/FeedbackExecutorService.js +4 -4
  32. package/dist/feedback-factory/execute/FeedbackExecutorService.js.map +1 -1
  33. package/dist/feedback-factory/execute/executePolicy.d.ts +3 -0
  34. package/dist/feedback-factory/execute/executePolicy.d.ts.map +1 -1
  35. package/dist/feedback-factory/execute/executePolicy.js +3 -0
  36. package/dist/feedback-factory/execute/executePolicy.js.map +1 -1
  37. package/dist/messaging/messageOrigin.d.ts +94 -0
  38. package/dist/messaging/messageOrigin.d.ts.map +1 -0
  39. package/dist/messaging/messageOrigin.js +246 -0
  40. package/dist/messaging/messageOrigin.js.map +1 -0
  41. package/dist/messaging/slack/FileHandler.d.ts +3 -1
  42. package/dist/messaging/slack/FileHandler.d.ts.map +1 -1
  43. package/dist/messaging/slack/FileHandler.js +9 -1
  44. package/dist/messaging/slack/FileHandler.js.map +1 -1
  45. package/dist/messaging/slack/SlackAdapter.d.ts +9 -0
  46. package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
  47. package/dist/messaging/slack/SlackAdapter.js +60 -4
  48. package/dist/messaging/slack/SlackAdapter.js.map +1 -1
  49. package/dist/messaging/slack/SlackOriginLedger.d.ts +97 -0
  50. package/dist/messaging/slack/SlackOriginLedger.d.ts.map +1 -0
  51. package/dist/messaging/slack/SlackOriginLedger.js +298 -0
  52. package/dist/messaging/slack/SlackOriginLedger.js.map +1 -0
  53. package/dist/messaging/telegram-origin/OriginAwareness.d.ts +3 -0
  54. package/dist/messaging/telegram-origin/OriginAwareness.d.ts.map +1 -1
  55. package/dist/messaging/telegram-origin/OriginAwareness.js +14 -0
  56. package/dist/messaging/telegram-origin/OriginAwareness.js.map +1 -1
  57. package/dist/messaging/telegram-origin/OriginStore.d.ts +5 -0
  58. package/dist/messaging/telegram-origin/OriginStore.d.ts.map +1 -1
  59. package/dist/messaging/telegram-origin/OriginStore.js +2 -1
  60. package/dist/messaging/telegram-origin/OriginStore.js.map +1 -1
  61. package/dist/messaging/telegram-origin/OriginStore.worker.js +1 -1
  62. package/dist/messaging/telegram-origin/OriginStore.worker.js.map +1 -1
  63. package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts +14 -0
  64. package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts.map +1 -1
  65. package/dist/messaging/telegram-origin/OriginStoreBackend.js +45 -0
  66. package/dist/messaging/telegram-origin/OriginStoreBackend.js.map +1 -1
  67. package/dist/messaging/telegram-origin/TelegramOriginService.d.ts +4 -0
  68. package/dist/messaging/telegram-origin/TelegramOriginService.d.ts.map +1 -1
  69. package/dist/messaging/telegram-origin/TelegramOriginService.js +10 -2
  70. package/dist/messaging/telegram-origin/TelegramOriginService.js.map +1 -1
  71. package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
  72. package/dist/native/instar-desktop-host +0 -0
  73. package/dist/scaffold/templates.d.ts.map +1 -1
  74. package/dist/scaffold/templates.js +3 -1
  75. package/dist/scaffold/templates.js.map +1 -1
  76. package/dist/server/AgentServer.d.ts.map +1 -1
  77. package/dist/server/AgentServer.js +3 -0
  78. package/dist/server/AgentServer.js.map +1 -1
  79. package/dist/server/mobileConversationRoutes.d.ts +17 -1
  80. package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
  81. package/dist/server/mobileConversationRoutes.js +31 -6
  82. package/dist/server/mobileConversationRoutes.js.map +1 -1
  83. package/dist/server/mobileSlackRoutes.d.ts +13 -0
  84. package/dist/server/mobileSlackRoutes.d.ts.map +1 -1
  85. package/dist/server/mobileSlackRoutes.js +14 -1
  86. package/dist/server/mobileSlackRoutes.js.map +1 -1
  87. package/dist/server/originDisplayPreferences.d.ts.map +1 -1
  88. package/dist/server/originDisplayPreferences.js +19 -8
  89. package/dist/server/originDisplayPreferences.js.map +1 -1
  90. package/dist/server/replyOrigin.d.ts +39 -0
  91. package/dist/server/replyOrigin.d.ts.map +1 -0
  92. package/dist/server/replyOrigin.js +89 -0
  93. package/dist/server/replyOrigin.js.map +1 -0
  94. package/dist/server/routes.d.ts.map +1 -1
  95. package/dist/server/routes.js +162 -4
  96. package/dist/server/routes.js.map +1 -1
  97. package/package.json +1 -1
  98. package/scripts/blocking-decision-surfaces-baseline.json +3 -3
  99. package/src/data/builtin-manifest.json +66 -66
  100. package/src/data/standards-guard-index.json +1 -1
  101. package/src/data/standards-guard-index.meta.json +2 -2
  102. package/src/data/standards-registry.meta.json +1 -1
  103. package/src/scaffold/templates.ts +4 -1
  104. package/src/templates/scripts/app-reply.sh +17 -0
  105. package/src/templates/scripts/slack-reply.sh +23 -0
  106. package/upgrades/1.3.1640.md +25 -0
  107. package/upgrades/feedback-confined-timeout.eli16.md +7 -0
  108. package/upgrades/side-effects/feedback-confined-timeout.md +80 -0
  109. package/upgrades/side-effects/message-origin-all-platforms.md +92 -0
@@ -1,4 +1,8 @@
1
1
  import { registerEcosystemIdeaRoutes } from './ecosystemIdeaRoutes.js';
2
+ import { ISSUES_TOPIC_OFFSET } from '../messaging/telegramTopicSpace.js';
3
+ import { SlackOriginLedger, SlackOriginRecordingError } from '../messaging/slack/SlackOriginLedger.js';
4
+ import { slackOriginFooter, currentAgentDisplay, deviceOriginRow, effectiveDisplay, originEvidenceFromProducer, validateDeviceOriginRow, validateMessageOriginEvidence } from '../messaging/messageOrigin.js';
5
+ import { resolveReplyOrigin, boundSessionName, localMachineIdentity } from './replyOrigin.js';
2
6
  import { stripViewSignatures } from './mobilePairingRoutes.js';
3
7
  import { parseAutoExpiryHold } from '../core/AutoExpiryLedger.js';
4
8
  import { getCodexDeviceLimitWiring } from '../core/codexDeviceLimitWiring.js';
@@ -4347,6 +4351,14 @@ export function createRoutes(ctx) {
4347
4351
  if (ctx.writeAdmission) {
4348
4352
  base.eventLoop = ctx.writeAdmission.eventLoopStats();
4349
4353
  }
4354
+ // message-origin-all-platforms §6: recording counters since boot (never message content).
4355
+ {
4356
+ const ledger = ctx.slack?.originLedger;
4357
+ base.messageOrigin = {
4358
+ app: { recorded: appOriginRecordedCount },
4359
+ slack: ledger ? { ...ledger.counters, coversSince: ledger.coversSince === null ? null : new Date(ledger.coversSince).toISOString() } : null,
4360
+ };
4361
+ }
4350
4362
  // Origin worker health rides the AUTHED extension: a held-sends outage is
4351
4363
  // visible without a status fan-out, and never inferred from silence.
4352
4364
  if (ctx.telegramOrigin) {
@@ -18754,6 +18766,71 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
18754
18766
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
18755
18767
  }
18756
18768
  });
18769
+ // message-origin-all-platforms §4/§5: the display bits for one Telegram topic (agent default, then
18770
+ // the topic's own override) and the batched origin join for a page of its rows.
18771
+ const telegramOriginDisplay = (topicId) => {
18772
+ let override;
18773
+ try {
18774
+ override = ctx.topicProfile?.store.resolve(String(topicId))?.messageOriginDisplay ?? undefined;
18775
+ }
18776
+ catch {
18777
+ override = undefined;
18778
+ } // @silent-fallback-ok — no readable override means the agent default
18779
+ return effectiveDisplay(currentAgentDisplay({ liveConfig: ctx.liveConfig, config: ctx.config }), override);
18780
+ };
18781
+ const ORIGIN_LOOKUP_BUDGET_MS = 750;
18782
+ const joinTelegramOrigin = async (topicId, rows, _unused) => {
18783
+ const display = telegramOriginDisplay(topicId);
18784
+ const agentRows = rows.filter(m => m.fromUser === false && (typeof m.messageId === 'number' || typeof m.messageId === 'string'));
18785
+ if (agentRows.length === 0)
18786
+ return rows.map(m => ({ ...m }));
18787
+ const tgConfig = ctx.config.messaging?.find(m => m.type === 'telegram')?.config;
18788
+ const issues = topicId >= ISSUES_TOPIC_OFFSET;
18789
+ const rawChat = issues ? tgConfig?.issuesChatId : (ctx.telegramOrigin?.options.bot.chatId ?? tgConfig?.chatId);
18790
+ const chatId = rawChat === undefined || rawChat === null || rawChat === '' ? null : String(rawChat);
18791
+ const store = ctx.telegramOrigin?.store;
18792
+ let found = null;
18793
+ let lookupFailure = 'lookup-failed';
18794
+ if (store && chatId) {
18795
+ try {
18796
+ let timer;
18797
+ const page = await Promise.race([
18798
+ store.lookupOriginEvidence({ transport: 'bot-api', chatId, messageIds: agentRows.slice(0, 100).map(m => String(m.messageId)) }),
18799
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('origin-lookup-timeout')), ORIGIN_LOOKUP_BUDGET_MS); }),
18800
+ ]).finally(() => { if (timer)
18801
+ clearTimeout(timer); });
18802
+ found = new Map(page.results.map(r => [r.messageId, r]));
18803
+ }
18804
+ catch (err) { // @silent-fallback-ok — rows read recorded:null with the reason, never a guess
18805
+ found = null;
18806
+ lookupFailure = err instanceof Error && err.message === 'origin-lookup-timeout' ? 'lookup-timeout' : 'lookup-failed';
18807
+ }
18808
+ }
18809
+ const now = Date.now();
18810
+ return rows.map(m => {
18811
+ if (m.fromUser !== false)
18812
+ return { ...m };
18813
+ const hit = found?.get(String(m.messageId));
18814
+ let origin;
18815
+ if (found === null)
18816
+ origin = deviceOriginRow(null, display, !(store && chatId) ? 'not-found' : lookupFailure);
18817
+ else if (!hit)
18818
+ origin = deviceOriginRow(null, display, 'not-found');
18819
+ else if (hit.state === 'ambiguous')
18820
+ origin = deviceOriginRow(null, display, 'ambiguous');
18821
+ else if (hit.state !== 'found' || !hit.producer)
18822
+ origin = deviceOriginRow(null, display, 'not-retained');
18823
+ else {
18824
+ const p = hit.producer;
18825
+ const evidence = p.machine && p.harness && p.model ? validateMessageOriginEvidence(originEvidenceFromProducer({
18826
+ producerKind: p.producerKind ?? 'session', originMachineId: String(p.originMachineId ?? ''), originMachineName: String(p.originMachineName ?? ''),
18827
+ harnessName: p.harnessName ?? null, machine: p.machine, harness: p.harness, model: p.model,
18828
+ }, now)) : null;
18829
+ origin = evidence ? deviceOriginRow(evidence, display) : deviceOriginRow(null, display, 'not-retained');
18830
+ }
18831
+ return { ...m, origin };
18832
+ });
18833
+ };
18757
18834
  router.get('/telegram/topics/:topicId/messages', async (req, res) => {
18758
18835
  if (!ctx.telegram) {
18759
18836
  res.status(503).json({ error: 'Telegram not configured' });
@@ -18789,9 +18866,13 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
18789
18866
  // Telegram media (mobile-native-chat-media §4.9): a device gets each recorded file as
18790
18867
  // `attachments`, and never a server path in the text.
18791
18868
  const joinForDevice = (rows) => (isDevice ? withTelegramMediaForDevice(ctx.appConversations ?? null, topicId, ctx.appConversations ? joinTelegramHistory(ctx.appConversations, topicId, rows, limit) : rows) : rows);
18869
+ // message-origin-all-platforms §4: agent rows carry `origin` for a device, and for a Bearer read
18870
+ // that asks (`includeOrigin=1`, set by the pool fan-out so each peer answers with its own join).
18871
+ const withOrigin = isDevice || req.query.includeOrigin === '1';
18792
18872
  if (req.query.scope !== 'pool') {
18793
18873
  const joined = joinForDevice(local);
18794
- res.json({ topicId, messages: isDevice ? stripViewSignatures(joined) : joined });
18874
+ const shaped = withOrigin ? await joinTelegramOrigin(topicId, joined, null) : joined;
18875
+ res.json({ topicId, messages: isDevice ? stripViewSignatures(shaped) : shaped });
18795
18876
  return;
18796
18877
  }
18797
18878
  // Pool read (mobile-operator-chat §2.4a): every answering machine's rows, tagged with the
@@ -18807,7 +18888,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
18807
18888
  failed.push({ machineId: p.machineId, error: 'peer URL refused' });
18808
18889
  return;
18809
18890
  }
18810
- const r = await poolPeerFetch(ctx.recordRopeResult ?? undefined, p, `/telegram/topics/${topicId}/messages?limit=${limit}${beforeMs === null ? '' : `&before=${beforeMs}`}`, {
18891
+ const r = await poolPeerFetch(ctx.recordRopeResult ?? undefined, p, `/telegram/topics/${topicId}/messages?limit=${limit}${beforeMs === null ? '' : `&before=${beforeMs}`}${withOrigin ? '&includeOrigin=1' : ''}`, {
18811
18892
  headers: { Authorization: `Bearer ${ctx.config.authToken}` }, signal: AbortSignal.timeout(5000), redirect: 'manual',
18812
18893
  });
18813
18894
  if (!r.ok) {
@@ -18834,6 +18915,28 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
18834
18915
  }
18835
18916
  }));
18836
18917
  const keyOf = (m) => typeof m.eventId === 'string' && m.eventId ? `e:${m.eventId}` : `m:${m.fromUser ? 'in' : 'out'}:${m.messageId}`;
18918
+ if (withOrigin) {
18919
+ // Own rows join here; a peer row's origin is re-checked (its display bits replaced by this
18920
+ // machine's). On a collision a peer's recorded origin fills in one this machine cannot tell.
18921
+ const own = await joinTelegramOrigin(topicId, rows.filter(m => m.machine === self), null);
18922
+ const ownByKey = new Map(own.map(m => [keyOf(m), m]));
18923
+ const display = telegramOriginDisplay(topicId);
18924
+ for (let i = 0; i < rows.length; i++) {
18925
+ const m = rows[i];
18926
+ if (m.machine === self) {
18927
+ rows[i] = ownByKey.get(keyOf(m)) ?? m;
18928
+ continue;
18929
+ }
18930
+ if (m.fromUser !== false) {
18931
+ delete m.origin;
18932
+ continue;
18933
+ }
18934
+ m.origin = validateDeviceOriginRow(m.origin, display) ?? deviceOriginRow(null, display, 'other-machine');
18935
+ const mine = ownByKey.get(keyOf(m));
18936
+ if (mine && mine.origin?.recorded !== true && m.origin.recorded === true)
18937
+ mine.origin = m.origin;
18938
+ }
18939
+ }
18837
18940
  // This machine's own rows win a key collision; a peer's row is untrusted display data.
18838
18941
  const seen = new Set(rows.filter(m => m.machine === self).map(keyOf));
18839
18942
  const merged = rows
@@ -19937,6 +20040,29 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
19937
20040
  }
19938
20041
  });
19939
20042
  // ── Slack ──────────────────────────────────────────────────────
20043
+ // message-origin-all-platforms §2: the evidence ladder for app and Slack replies (no refusal here).
20044
+ /** App agent replies stored with their origin since boot (§6). */
20045
+ let appOriginRecordedCount = 0;
20046
+ const localMachine = localMachineIdentity(ctx.config.stateDir);
20047
+ const replyOriginDeps = (machineIdOverride) => ({
20048
+ machine: () => {
20049
+ const local = localMachine();
20050
+ const selfId = machineIdOverride ?? local.id ?? ctx.meshSelfId ?? null;
20051
+ const name = ctx.telegramOrigin?.options.identity.originMachineName ?? local.name
20052
+ ?? (ctx.meshSelfId ? ctx.machinePoolRegistry?.getCapacity(ctx.meshSelfId)?.nickname ?? null : null);
20053
+ return { id: selfId, name: name ?? null };
20054
+ },
20055
+ originService: () => ctx.telegramOrigin?.service ?? null,
20056
+ runningSessionByTmux: (tmuxName) => {
20057
+ const s = ctx.state.listSessions({ status: 'running' }).find((x) => x.tmuxSession === tmuxName);
20058
+ return s ? { id: s.id, framework: s.framework ?? null, model: typeof s.model === 'string' ? s.model : null } : null;
20059
+ },
20060
+ tmuxForSessionId: (sessionId) => ctx.state.getSession(sessionId)?.tmuxSession ?? null,
20061
+ });
20062
+ const originHeader = (req) => {
20063
+ const raw = req.headers['x-instar-origin-session'];
20064
+ return Array.isArray(raw) ? raw[0] : raw;
20065
+ };
19940
20066
  const handleSlackReply = async (req, res) => {
19941
20067
  if (!ctx.slack) {
19942
20068
  res.status(503).json({ error: 'Slack not configured' });
@@ -19995,7 +20121,27 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
19995
20121
  // `finalize-ambiguous`, NEVER re-posted), NOT the 500 catch-all below
19996
20122
  // (500 → recovery-policy retry → double-post). The still-alive adapter
19997
20123
  // call is abandoned; it must NOT record a delivery-id on this path.
19998
- const ts = await sendWithAdapterTimeout(() => ctx.slack.sendToChannel(channelId, text, { thread_ts, formatMode }), SLACK_ADAPTER_SEND_TIMEOUT_MS);
20124
+ // message-origin-all-platforms §2/§3: the reply's origin, scoped to exactly this one send to
20125
+ // this target, plus the one-line footer the agent's display bits allow.
20126
+ // The same channel/thread the adapter will post to (sendToChannel: options.thread_ts ?? the routing key's thread).
20127
+ const [scopeChannel, routedThread] = String(channelId).split(':');
20128
+ const effectiveThread = thread_ts ?? routedThread;
20129
+ const scopeThread = typeof effectiveThread === 'string' && effectiveThread ? effectiveThread : null;
20130
+ const replyOrigin = await resolveReplyOrigin(replyOriginDeps(), {
20131
+ boundSession: typeof res.locals.originBoundSession === 'string' ? res.locals.originBoundSession : null,
20132
+ originCredential: originHeader(req),
20133
+ });
20134
+ let originFooter = '';
20135
+ try {
20136
+ originFooter = slackOriginFooter(ctx.config.projectName ?? 'agent', replyOrigin, currentAgentDisplay({ liveConfig: ctx.liveConfig, config: ctx.config }));
20137
+ // The labels are outgoing content: the deterministic credential wall sees them too (§5).
20138
+ if (originFooter && detectOutboundCredential(originFooter).detected)
20139
+ originFooter = '';
20140
+ }
20141
+ catch {
20142
+ originFooter = '';
20143
+ } // @silent-fallback-ok — the footer is presentation; it never fails the send
20144
+ const ts = await sendWithAdapterTimeout(() => SlackOriginLedger.runInScope({ evidence: replyOrigin, channel: scopeChannel, threadTs: scopeThread }, () => ctx.slack.sendToChannel(channelId, text, { thread_ts, formatMode, ...(originFooter ? { originFooter } : {}) })), SLACK_ADAPTER_SEND_TIMEOUT_MS);
19999
20145
  // Record the delivery-id ONLY after a successful send (spec §2.4) — a
20000
20146
  // failed send (which throws before here, incl. the ambiguous 408
20001
20147
  // timeout) must not poison the id so its legitimate retry isn't lost.
@@ -20044,6 +20190,12 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
20044
20190
  res.status(408).json({ error: 'adapter-send-timeout', ambiguous: true });
20045
20191
  return;
20046
20192
  }
20193
+ // message-origin-all-platforms §3: no origin record, no send. Nothing left; say so plainly.
20194
+ if (err instanceof SlackOriginRecordingError) {
20195
+ res.status(503).json({ error: 'origin-recording-unavailable', notSent: true,
20196
+ detail: 'Not sent: this machine could not record which machine and model wrote the reply.' });
20197
+ return;
20198
+ }
20047
20199
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
20048
20200
  }
20049
20201
  };
@@ -20077,6 +20229,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
20077
20229
  res.status(403).json({ error: 'conversation-bind-refused', detail: verdict.detail });
20078
20230
  return;
20079
20231
  }
20232
+ res.locals.originBoundSession = boundSessionName(verdict.boundBy);
20080
20233
  const target = ctx.conversationRegistry?.resolve(conversationId);
20081
20234
  if (!target || target.platform !== 'slack' || target.origin === 'replicated') {
20082
20235
  res.status(409).json({ error: 'slack-adapter-authority-unavailable' });
@@ -20150,6 +20303,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
20150
20303
  res.status(403).json({ error: 'conversation-bind-refused', detail: verdict.detail });
20151
20304
  return;
20152
20305
  }
20306
+ const appBoundSession = boundSessionName(verdict.boundBy);
20153
20307
  const key = ctx.conversationRegistry?.appConversationFor(conversationId) ?? null;
20154
20308
  if (!key || !app.liveConversation(key)) {
20155
20309
  res.status(404).json({ error: 'unknown-conversation' });
@@ -20213,8 +20367,10 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
20213
20367
  return;
20214
20368
  }
20215
20369
  const messageId = derivedEventId('am', key, seed);
20370
+ // message-origin-all-platforms §3: the origin is part of the same append as the reply.
20371
+ const agentOrigin = await resolveReplyOrigin(replyOriginDeps(app.deps.machineId()), { boundSession: appBoundSession, originCredential: originHeader(req) });
20216
20372
  try {
20217
- const common = { eventId, conversationKey: key, messageId, role: 'agent', text, at: new Date().toISOString(), machineId: app.deps.machineId() };
20373
+ const common = { eventId, conversationKey: key, messageId, role: 'agent', text, at: new Date().toISOString(), machineId: app.deps.machineId(), agentOrigin };
20218
20374
  if (preparedFiles) {
20219
20375
  // Publish the snapshots that were scanned, then the event: never an event without its blobs.
20220
20376
  const attachments = preparedFiles.snapshots.map((snap, index) => {
@@ -20227,11 +20383,13 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
20227
20383
  };
20228
20384
  });
20229
20385
  app.store.append({ ...common, kind: 'media-message', attachments });
20386
+ appOriginRecordedCount++;
20230
20387
  for (const id of preparedFiles.reservationIds)
20231
20388
  app.media.releaseReply(id);
20232
20389
  }
20233
20390
  else {
20234
20391
  app.store.append({ ...common, kind: 'message' });
20392
+ appOriginRecordedCount++;
20235
20393
  }
20236
20394
  }
20237
20395
  catch (err) {