cli-jaw 2.2.25 → 2.3.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.
Files changed (159) hide show
  1. package/README.md +5 -2
  2. package/dist/bin/cli-jaw.js +6 -1
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/doctor.js +39 -18
  5. package/dist/bin/commands/doctor.js.map +1 -1
  6. package/dist/bin/commands/init.js +82 -23
  7. package/dist/bin/commands/init.js.map +1 -1
  8. package/dist/bin/commands/messaging.js +262 -0
  9. package/dist/bin/commands/messaging.js.map +1 -0
  10. package/dist/server.js +30 -6
  11. package/dist/server.js.map +1 -1
  12. package/dist/src/cli/commands.js +6 -2
  13. package/dist/src/cli/commands.js.map +1 -1
  14. package/dist/src/cli/handlers/remote-session-commands.js +94 -0
  15. package/dist/src/cli/handlers/remote-session-commands.js.map +1 -0
  16. package/dist/src/command-contract/catalog.js +1 -1
  17. package/dist/src/command-contract/catalog.js.map +1 -1
  18. package/dist/src/core/chat-sessions.js +4 -0
  19. package/dist/src/core/chat-sessions.js.map +1 -1
  20. package/dist/src/core/config.js +63 -22
  21. package/dist/src/core/config.js.map +1 -1
  22. package/dist/src/core/db.js +6 -0
  23. package/dist/src/core/db.js.map +1 -1
  24. package/dist/src/core/dispatch-approval-ingress.js +45 -3
  25. package/dist/src/core/dispatch-approval-ingress.js.map +1 -1
  26. package/dist/src/core/dispatch-approval.js +46 -0
  27. package/dist/src/core/dispatch-approval.js.map +1 -1
  28. package/dist/src/core/logger.js +67 -0
  29. package/dist/src/core/logger.js.map +1 -1
  30. package/dist/src/core/session-generation.js +74 -0
  31. package/dist/src/core/session-generation.js.map +1 -0
  32. package/dist/src/discord/bot.js +91 -5
  33. package/dist/src/discord/bot.js.map +1 -1
  34. package/dist/src/discord/commands.js +34 -1
  35. package/dist/src/discord/commands.js.map +1 -1
  36. package/dist/src/discord/forwarder.js +4 -1
  37. package/dist/src/discord/forwarder.js.map +1 -1
  38. package/dist/src/discord/send-only-client.js +14 -7
  39. package/dist/src/discord/send-only-client.js.map +1 -1
  40. package/dist/src/messaging/access-policy.js +26 -0
  41. package/dist/src/messaging/access-policy.js.map +1 -0
  42. package/dist/src/messaging/approval-presentation.js +89 -0
  43. package/dist/src/messaging/approval-presentation.js.map +1 -0
  44. package/dist/src/messaging/channel-adapter.js +47 -0
  45. package/dist/src/messaging/channel-adapter.js.map +1 -0
  46. package/dist/src/messaging/channel-capabilities.js +47 -15
  47. package/dist/src/messaging/channel-capabilities.js.map +1 -1
  48. package/dist/src/messaging/channel-health.js +23 -3
  49. package/dist/src/messaging/channel-health.js.map +1 -1
  50. package/dist/src/messaging/delivery-outcome.js +12 -0
  51. package/dist/src/messaging/delivery-outcome.js.map +1 -1
  52. package/dist/src/messaging/durable-ingress.js +458 -0
  53. package/dist/src/messaging/durable-ingress.js.map +1 -0
  54. package/dist/src/messaging/effect-once.js +204 -0
  55. package/dist/src/messaging/effect-once.js.map +1 -0
  56. package/dist/src/messaging/inbound-envelope.js +168 -0
  57. package/dist/src/messaging/inbound-envelope.js.map +1 -0
  58. package/dist/src/messaging/ingress-audit.js +41 -0
  59. package/dist/src/messaging/ingress-audit.js.map +1 -0
  60. package/dist/src/messaging/ingress-generation.js +22 -0
  61. package/dist/src/messaging/ingress-generation.js.map +1 -0
  62. package/dist/src/messaging/metrics.js +69 -0
  63. package/dist/src/messaging/metrics.js.map +1 -0
  64. package/dist/src/messaging/outbound-outbox.js +184 -0
  65. package/dist/src/messaging/outbound-outbox.js.map +1 -0
  66. package/dist/src/messaging/remote-command-context.js +30 -0
  67. package/dist/src/messaging/remote-command-context.js.map +1 -0
  68. package/dist/src/messaging/retry.js +11 -2
  69. package/dist/src/messaging/retry.js.map +1 -1
  70. package/dist/src/messaging/runtime.js +129 -30
  71. package/dist/src/messaging/runtime.js.map +1 -1
  72. package/dist/src/messaging/send.js +31 -6
  73. package/dist/src/messaging/send.js.map +1 -1
  74. package/dist/src/messaging/trace-context.js +56 -0
  75. package/dist/src/messaging/trace-context.js.map +1 -0
  76. package/dist/src/messaging/types.js +39 -1
  77. package/dist/src/messaging/types.js.map +1 -1
  78. package/dist/src/routes/orchestrate.js +10 -3
  79. package/dist/src/routes/orchestrate.js.map +1 -1
  80. package/dist/src/routes/settings.js +26 -1
  81. package/dist/src/routes/settings.js.map +1 -1
  82. package/dist/src/slack/bot.js +182 -70
  83. package/dist/src/slack/bot.js.map +1 -1
  84. package/dist/src/slack/commands.js +16 -1
  85. package/dist/src/slack/commands.js.map +1 -1
  86. package/dist/src/slack/format.js +4 -1
  87. package/dist/src/slack/format.js.map +1 -1
  88. package/dist/src/slack/manifest.js +5 -0
  89. package/dist/src/slack/manifest.js.map +1 -1
  90. package/dist/src/slack/send-handler.js +26 -4
  91. package/dist/src/slack/send-handler.js.map +1 -1
  92. package/dist/src/slack/send-only-client.js +26 -2
  93. package/dist/src/slack/send-only-client.js.map +1 -1
  94. package/dist/src/slack/socket.js +36 -13
  95. package/dist/src/slack/socket.js.map +1 -1
  96. package/dist/src/telegram/bot.js +122 -14
  97. package/dist/src/telegram/bot.js.map +1 -1
  98. package/package.json +4 -1
  99. package/public/dist/assets/ChannelEnablementControl-Da_t_jq6.js +1 -0
  100. package/public/dist/assets/ChannelsDiscord-kNjCCJcA.js +1 -0
  101. package/public/dist/assets/ChannelsSlack-BOuoCfeN.js +1 -0
  102. package/public/dist/assets/ChannelsTelegram-DBIFDz4R.js +1 -0
  103. package/public/dist/assets/{app-DUbf-v7B.js → app-TWwXw3iD.js} +4 -4
  104. package/public/dist/assets/bgtask-badge-BhzSqQHG.js +1 -0
  105. package/public/dist/assets/{bgtask-badge-jIH69Dp3.js → bgtask-badge-DaAUnoKD.js} +1 -1
  106. package/public/dist/assets/{employees-BNy8a6XX.js → employees-BFKkaVAe.js} +1 -1
  107. package/public/dist/assets/iframe-renderer-BxEzdKIA.js +1 -0
  108. package/public/dist/assets/{iframe-renderer-C6Zwg9w4.js → iframe-renderer-CvnKi77F.js} +2 -2
  109. package/public/dist/assets/{manager-DzCR6Pam.js → manager-BTpvAFwX.js} +1 -1
  110. package/public/dist/assets/memory-DHOtCiw5.js +1 -0
  111. package/public/dist/assets/{memory-C4O44HMr.js → memory-db_s-38T.js} +1 -1
  112. package/public/dist/assets/{message-history-DCxD21J1.js → message-history-BX9shNKA.js} +1 -1
  113. package/public/dist/assets/message-history-DaSxkyy0.js +1 -0
  114. package/public/dist/assets/{render-A--hkukv.js → render-DZnFK8Kv.js} +1 -1
  115. package/public/dist/assets/{settings-3ZdoKHIf.js → settings-BuFMKcQ-.js} +1 -1
  116. package/public/dist/assets/settings-DotRDITm.js +1 -0
  117. package/public/dist/assets/{settings-core-CQe1uP6x.js → settings-core-3fj0DVoa.js} +17 -17
  118. package/public/dist/assets/settings-core-DohSnMad.js +1 -0
  119. package/public/dist/assets/{sidebar-DxFp_449.js → sidebar-D5TP7TCO.js} +3 -3
  120. package/public/dist/assets/{skills-BU4q8Q8n.js → skills-CQzHFYJS.js} +1 -1
  121. package/public/dist/assets/skills-DZHEvl1x.js +1 -0
  122. package/public/dist/assets/{slash-commands-BiXOgZ5B.js → slash-commands-BWEWWf-c.js} +1 -1
  123. package/public/dist/assets/slash-commands-CuJdloNC.js +1 -0
  124. package/public/dist/assets/{trace-drawer-2BTQCor6.js → trace-drawer-CBR-PZB4.js} +1 -1
  125. package/public/dist/assets/ui-CWl27eMl.js +1 -0
  126. package/public/dist/assets/{ui-Csy42uNW.js → ui-Ca1Q-VFQ.js} +1 -1
  127. package/public/dist/index.html +1 -1
  128. package/public/dist/manager/index.html +1 -1
  129. package/public/js/features/settings-channel.ts +35 -5
  130. package/public/js/features/settings-types.ts +7 -0
  131. package/public/js/features/transport-status-row.ts +23 -4
  132. package/public/manager/src/settings/pages/ChannelsDiscord.tsx +39 -11
  133. package/public/manager/src/settings/pages/ChannelsSlack.tsx +24 -7
  134. package/public/manager/src/settings/pages/ChannelsTelegram.tsx +38 -10
  135. package/public/manager/src/settings/pages/components/ChannelEnablementControl.tsx +98 -0
  136. package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +23 -2
  137. package/scripts/docs/extract-commands.mts +3 -0
  138. package/scripts/generate-channel-capability-table.mts +161 -0
  139. package/scripts/promote-to-main.sh +175 -0
  140. package/scripts/release-gates.mjs +57 -3
  141. package/scripts/release-preview.sh +19 -0
  142. package/scripts/require-release-evidence.mjs +35 -1
  143. package/scripts/validate-messaging-certification.mts +83 -0
  144. package/scripts/write-messaging-certification.mts +65 -0
  145. package/public/dist/assets/ChannelsDiscord-D4fD-9hP.js +0 -1
  146. package/public/dist/assets/ChannelsSlack-_KBwRonM.js +0 -1
  147. package/public/dist/assets/ChannelsTelegram-tihzSWVE.js +0 -1
  148. package/public/dist/assets/TransportStatusChips-B0NR0ljO.js +0 -1
  149. package/public/dist/assets/bgtask-badge-B8KhuBa7.js +0 -1
  150. package/public/dist/assets/iframe-renderer-DBmV8gCQ.js +0 -1
  151. package/public/dist/assets/memory-Dt_Ji6aP.js +0 -1
  152. package/public/dist/assets/message-history-DyISauiv.js +0 -1
  153. package/public/dist/assets/settings-C6Pku2GC.js +0 -1
  154. package/public/dist/assets/settings-core-DqwLJkgP.js +0 -1
  155. package/public/dist/assets/skills-x2gH-1Ry.js +0 -1
  156. package/public/dist/assets/slash-commands-s8eR7Kd1.js +0 -1
  157. package/public/dist/assets/ui-CB0L33o0.js +0 -1
  158. package/scripts/release.sh +0 -224
  159. /package/public/dist/assets/{HealthBadge-CUaJ1PQH.js → HealthBadge-CiNMS-1k.js} +0 -0
@@ -0,0 +1,204 @@
1
+ // ─── Effect-once claims ──────────────────────────────────
2
+ // At-least-once ingress means a protected effect can be reached twice. This table is
3
+ // how the second arrival learns the first one already happened.
4
+ //
5
+ // A lease that has run out does NOT mean the effect failed. The owning process may
6
+ // have died a millisecond before or a millisecond after the external write. Expiry
7
+ // therefore permits a new CLAIM and nothing else; deciding what actually happened is
8
+ // a per-effect reconciler's job, and until it decides, `manual` is the honest state.
9
+ //
10
+ // The connection is injected for the same reason the journal injects it: reaching for
11
+ // the db singleton makes the table exist as a side effect of whoever imported first.
12
+ import { randomUUID } from 'node:crypto';
13
+ import { registerChildRetentionPredicate } from './durable-ingress.js';
14
+ export const EFFECT_CLAIMS_TABLE = 'effect_claims';
15
+ /** Local DB writes: a process that holds one this long is not coming back. */
16
+ export const LOCAL_EFFECT_LEASE_MS = 30_000;
17
+ /** Process spawn and remote tool calls legitimately take longer. */
18
+ export const REMOTE_EFFECT_LEASE_MS = 120_000;
19
+ export const CREATE_EFFECT_CLAIMS_SQL = `
20
+ CREATE TABLE IF NOT EXISTS effect_claims (
21
+ channel TEXT NOT NULL CHECK(channel IN ('telegram', 'slack', 'discord')),
22
+ account_id TEXT NOT NULL,
23
+ event_id TEXT NOT NULL,
24
+ effect_name TEXT NOT NULL,
25
+ claim_token TEXT NOT NULL UNIQUE,
26
+ state TEXT NOT NULL DEFAULT 'claimed'
27
+ CHECK(state IN ('claimed', 'completed', 'failed', 'manual')),
28
+ owner_id TEXT,
29
+ lease_expires_at INTEGER,
30
+ result_digest TEXT,
31
+ claimed_at INTEGER NOT NULL,
32
+ completed_at INTEGER,
33
+ last_error TEXT,
34
+ PRIMARY KEY (channel, account_id, event_id, effect_name),
35
+ FOREIGN KEY (channel, account_id, event_id)
36
+ REFERENCES ingress_events(channel, account_id, event_id)
37
+ );
38
+ CREATE INDEX IF NOT EXISTS idx_effect_claims_state
39
+ ON effect_claims (state, claimed_at);
40
+ `;
41
+ const STATES = new Set(['claimed', 'completed', 'failed', 'manual']);
42
+ function rowToRecord(row) {
43
+ const state = String(row['state']);
44
+ if (!STATES.has(state)) {
45
+ // Refused on read rather than guessed at: an unknown state here would otherwise
46
+ // be treated as "not terminal" and re-claimed.
47
+ throw new Error(`effect claims: unrecognised state ${JSON.stringify(state)}`);
48
+ }
49
+ return {
50
+ channel: String(row['channel']),
51
+ accountId: String(row['account_id']),
52
+ eventId: String(row['event_id']),
53
+ effectName: String(row['effect_name']),
54
+ claimToken: String(row['claim_token']),
55
+ state: state,
56
+ ownerId: row['owner_id'] ?? null,
57
+ leaseExpiresAt: row['lease_expires_at'] ?? null,
58
+ resultDigest: row['result_digest'] ?? null,
59
+ claimedAt: Number(row['claimed_at']),
60
+ completedAt: row['completed_at'] ?? null,
61
+ lastError: row['last_error'] ?? null,
62
+ };
63
+ }
64
+ export class EffectClaimStore {
65
+ database;
66
+ now;
67
+ ownerId;
68
+ constructor(database, options = {}) {
69
+ this.database = database;
70
+ this.now = options.now ?? (() => Date.now());
71
+ this.ownerId = options.ownerId ?? `${randomUUID()}:${randomUUID()}`;
72
+ this.database.exec(CREATE_EFFECT_CLAIMS_SQL);
73
+ registerChildRetentionPredicate({
74
+ table: EFFECT_CLAIMS_TABLE,
75
+ // A claim that has not reached a terminal state still describes work whose
76
+ // outcome nobody knows. Sweeping its parent would erase the only record.
77
+ blockingExistsSql: "SELECT 1 FROM effect_claims c WHERE c.channel = e.channel "
78
+ + "AND c.account_id = e.account_id AND c.event_id = e.event_id "
79
+ + "AND c.state NOT IN ('completed', 'failed')",
80
+ deleteTerminalSql: "DELETE FROM effect_claims WHERE channel = ? AND account_id = ? "
81
+ + "AND event_id = ? AND state IN ('completed', 'failed')",
82
+ });
83
+ }
84
+ get owner() { return this.ownerId; }
85
+ find(key) {
86
+ const row = this.database.prepare(`
87
+ SELECT * FROM effect_claims
88
+ WHERE channel = ? AND account_id = ? AND event_id = ? AND effect_name = ?
89
+ `).get(key.channel, key.accountId, key.eventId, key.effectName);
90
+ return row ? rowToRecord(row) : null;
91
+ }
92
+ /**
93
+ * Take ownership of one effect, or report why not.
94
+ *
95
+ * BEGIN IMMEDIATE plus an observed-token CAS is what makes two live workers safe:
96
+ * both can read the same expired row, only one can write it, and the loser sees
97
+ * `lease_held` rather than running the effect body a second time.
98
+ */
99
+ claim(key, leaseMs = LOCAL_EFFECT_LEASE_MS) {
100
+ const run = this.database.transaction(() => {
101
+ const now = this.now();
102
+ const existing = this.find(key);
103
+ if (!existing) {
104
+ const claimToken = randomUUID();
105
+ this.database.prepare(`
106
+ INSERT INTO effect_claims (
107
+ channel, account_id, event_id, effect_name, claim_token,
108
+ state, owner_id, lease_expires_at, claimed_at
109
+ ) VALUES (?, ?, ?, ?, ?, 'claimed', ?, ?, ?)
110
+ `).run(key.channel, key.accountId, key.eventId, key.effectName, claimToken, this.ownerId, now + leaseMs, now);
111
+ const record = this.find(key);
112
+ if (!record)
113
+ throw new Error('effect claims: claim did not persist');
114
+ return { acquired: true, claimToken, record };
115
+ }
116
+ if (existing.state !== 'claimed') {
117
+ // completed/failed/manual are all answers. Only an operator or a replay
118
+ // may reopen one, and neither goes through this path.
119
+ return { acquired: false, reason: 'terminal', record: existing };
120
+ }
121
+ if (existing.leaseExpiresAt !== null && existing.leaseExpiresAt > now) {
122
+ return { acquired: false, reason: 'lease_held', record: existing };
123
+ }
124
+ const claimToken = randomUUID();
125
+ const changes = this.database.prepare(`
126
+ UPDATE effect_claims
127
+ SET owner_id = ?, claim_token = ?, claimed_at = ?, lease_expires_at = ?, last_error = NULL
128
+ WHERE channel = ? AND account_id = ? AND event_id = ? AND effect_name = ?
129
+ AND state = 'claimed' AND claim_token = ?
130
+ `).run(this.ownerId, claimToken, now, now + leaseMs, key.channel, key.accountId, key.eventId, key.effectName, existing.claimToken).changes;
131
+ if (changes !== 1) {
132
+ const latest = this.find(key);
133
+ return { acquired: false, reason: 'lease_held', record: latest ?? existing };
134
+ }
135
+ const record = this.find(key);
136
+ if (!record)
137
+ throw new Error('effect claims: re-claim did not persist');
138
+ return { acquired: true, claimToken, record };
139
+ });
140
+ return run.immediate();
141
+ }
142
+ /** Extend a lease this worker still owns. Long effects heartbeat rather than
143
+ * taking a lease long enough to hide a dead process. */
144
+ heartbeat(key, claimToken, leaseMs = LOCAL_EFFECT_LEASE_MS) {
145
+ return this.database.prepare(`
146
+ UPDATE effect_claims SET lease_expires_at = ?
147
+ WHERE channel = ? AND account_id = ? AND event_id = ? AND effect_name = ?
148
+ AND state = 'claimed' AND owner_id = ? AND claim_token = ?
149
+ `).run(this.now() + leaseMs, key.channel, key.accountId, key.eventId, key.effectName, this.ownerId, claimToken).changes === 1;
150
+ }
151
+ settle(key, claimToken, state, extra = {}) {
152
+ const run = this.database.transaction(() => this.database.prepare(`
153
+ UPDATE effect_claims
154
+ SET state = ?, completed_at = ?, lease_expires_at = NULL,
155
+ result_digest = COALESCE(?, result_digest),
156
+ last_error = ?
157
+ WHERE channel = ? AND account_id = ? AND event_id = ? AND effect_name = ?
158
+ AND state = 'claimed' AND owner_id = ? AND claim_token = ?
159
+ `).run(state, this.now(), extra.resultDigest ?? null, extra.lastError ?? null, key.channel, key.accountId, key.eventId, key.effectName, this.ownerId, claimToken).changes === 1);
160
+ return run.immediate();
161
+ }
162
+ /** The external effect is known to have happened. */
163
+ complete(key, claimToken, resultDigest) {
164
+ return this.settle(key, claimToken, 'completed', resultDigest === undefined ? {} : { resultDigest });
165
+ }
166
+ /** Known NOT to have happened, so a later replay may legitimately retry it. */
167
+ fail(key, claimToken, lastError) {
168
+ return this.settle(key, claimToken, 'failed', { lastError });
169
+ }
170
+ /** Cannot be decided automatically. Terminal until an operator says otherwise;
171
+ * guessing here is how a payment sends twice or a reset silently does not. */
172
+ holdForManual(key, claimToken, lastError) {
173
+ return this.settle(key, claimToken, 'manual', { lastError });
174
+ }
175
+ list(filter = {}) {
176
+ const where = filter.state ? 'WHERE state = ?' : '';
177
+ const params = filter.state ? [filter.state] : [];
178
+ params.push(filter.limit ?? 50);
179
+ const rows = this.database.prepare(`SELECT * FROM effect_claims ${where} ORDER BY claimed_at DESC LIMIT ?`).all(...params);
180
+ return rows.map(rowToRecord);
181
+ }
182
+ /** Rows whose lease has run out. Candidates for a reconciler, not for a re-run. */
183
+ listExpired(now = this.now(), limit = 50) {
184
+ const rows = this.database.prepare(`
185
+ SELECT * FROM effect_claims
186
+ WHERE state = 'claimed' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?
187
+ ORDER BY lease_expires_at, claimed_at LIMIT ?
188
+ `).all(now, limit);
189
+ return rows.map(rowToRecord);
190
+ }
191
+ }
192
+ let store = null;
193
+ export function initEffectClaimStore(database, options = {}) {
194
+ store = new EffectClaimStore(database, options);
195
+ return store;
196
+ }
197
+ export function getEffectClaimStore() {
198
+ return store;
199
+ }
200
+ /** Test seam: the module-level handle outlives a single test otherwise. */
201
+ export function __resetEffectClaimStoreForTests() {
202
+ store = null;
203
+ }
204
+ //# sourceMappingURL=effect-once.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect-once.js","sourceRoot":"","sources":["../../../src/messaging/effect-once.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ // ─── Inbound Envelope Normalizers ────────────────────
2
+ // One pure function per vendor, turning a vendor inbound event into an
3
+ // `InboundEnvelope`. Nothing here reaches a network, a clock it was not handed,
4
+ // or a vendor SDK: the transport handler collects the fields it already has and
5
+ // calls in.
6
+ //
7
+ // Deliberately NOT importing `src/telegram/bot.ts`, `src/slack/bot.ts` or
8
+ // `src/discord/bot.ts`. Those modules run at import time (bot singletons, dedupe
9
+ // stores, prepared SQLite statements) and drag in the vendor SDKs — discord.js
10
+ // alone costs ~48MB RSS. A normalizer that cannot be imported without starting a
11
+ // transport is not testable, so the inputs are narrow structural types declared
12
+ // locally instead.
13
+ //
14
+ // Every function returns `null` when a required field cannot be determined. The
15
+ // caller treats null as "do not admit this event". Fabricating a placeholder
16
+ // (`'unknown'`, `0`, `''`) would be worse than dropping: `accountId` namespaces
17
+ // the ingress journal, so a fake one silently merges two workspaces' event keys.
18
+ /** Build the opaque correlation handle for logs and traces.
19
+ *
20
+ * It exists to join a core-side log line back to a vendor delivery when
21
+ * debugging, and that is ALL it may carry. Never put a token, a message body, a
22
+ * file name or an attachment URL in here: this string is written to logs and
23
+ * traces that are not redacted downstream and are routinely pasted into issues.
24
+ * Vendor ids are safe because they are meaningless without the credential. */
25
+ const rawRef = (kind, id) => `${kind}:${id}`;
26
+ /**
27
+ * Telegram: `receivedAt` comes from the adapter clock, not the update.
28
+ *
29
+ * A Telegram update does carry `message.date`, but this tree never extracts it —
30
+ * no handler reads it and no existing field is derived from it. It is also a SEND
31
+ * time in whole seconds, not a receive time, and it is absent on several update
32
+ * kinds this path admits. Deriving `receivedAt` from it would silently change
33
+ * meaning per update type, so the honest value is the moment this process
34
+ * observed the update. The clock is injectable purely so tests are deterministic.
35
+ */
36
+ export function telegramInboundEnvelope(input) {
37
+ const accountId = idString(input.botUserId);
38
+ const eventId = idString(input.updateId);
39
+ const chatId = idString(input.chatId);
40
+ const actorId = idString(input.fromId);
41
+ if (!accountId || !eventId || !chatId || !actorId)
42
+ return null;
43
+ if (input.target.channel !== 'telegram')
44
+ return null;
45
+ const threadKey = telegramThreadKey(input);
46
+ return {
47
+ channel: 'telegram',
48
+ accountId,
49
+ eventId,
50
+ conversationKey: `telegram:${chatId}`,
51
+ ...(threadKey ? { threadKey } : {}),
52
+ actorId,
53
+ receivedAt: (input.now ?? Date.now)(),
54
+ // Telegram's polling offset only advances once the reply is delivered, so a
55
+ // crash mid-run redelivers the update. A transport property, not a setting.
56
+ ackPolicy: 'after-final-delivery',
57
+ rawEnvelopeRef: rawRef('telegram:update', eventId),
58
+ target: input.target,
59
+ };
60
+ }
61
+ /** Topic id 1 is the forum's General topic, which is the chat itself rather than a
62
+ * distinct thread. `buildTelegramTarget` excludes it from `threadId`; excluding it
63
+ * here too keeps `threadKey` and `target.threadId` from disagreeing. */
64
+ function telegramThreadKey(input) {
65
+ if (!input.isTopicMessage)
66
+ return undefined;
67
+ const raw = input.messageThreadId;
68
+ if (raw == null)
69
+ return undefined;
70
+ const numeric = Number(raw);
71
+ if (!Number.isFinite(numeric) || numeric <= 1)
72
+ return undefined;
73
+ return String(raw);
74
+ }
75
+ /**
76
+ * Slack: `receivedAt` is derived from `event.ts`, which unlike the other two
77
+ * vendors IS a canonical event time (epoch seconds with microsecond fraction).
78
+ * An unparseable ts drops the event rather than falling back to the local clock —
79
+ * `ts` is also the event identity, so a ts we cannot read means we cannot dedupe.
80
+ */
81
+ export function slackInboundEnvelope(input) {
82
+ const accountId = (input.teamId ?? '').trim();
83
+ const channelId = (input.channelId ?? '').trim();
84
+ const ts = (input.ts ?? '').trim();
85
+ const actorId = (input.userId ?? '').trim() || (input.botId ?? '').trim();
86
+ if (!accountId || !channelId || !ts || !actorId)
87
+ return null;
88
+ if (input.target.channel !== 'slack')
89
+ return null;
90
+ const receivedAt = Number(ts) * 1000;
91
+ if (!Number.isFinite(receivedAt))
92
+ return null;
93
+ // Same shape as `slackEventKey` in src/slack/ingress.ts, the source of truth for
94
+ // the ingress dedupe key. Reimplemented rather than imported because that module
95
+ // opens the SQLite dedupe store at import time. Its `'unknown'` team fallback is
96
+ // unreachable from here: an empty team already returned null.
97
+ const eventId = `${accountId}:${channelId}:${ts}`;
98
+ const threadTs = (input.threadTs ?? '').trim();
99
+ // The PARENT ts identifies the thread. A top-level message has none, so its own
100
+ // ts becomes the parent of the thread a reply would open (see
101
+ // `resolveSlackThreadTs`). With replyInThread off no thread is opened at all.
102
+ const threadKey = threadTs || (input.replyInThread !== false ? ts : '');
103
+ const envelopeId = (input.envelopeId ?? '').trim();
104
+ return {
105
+ channel: 'slack',
106
+ accountId,
107
+ eventId,
108
+ conversationKey: `slack:${accountId}:${channelId}`,
109
+ ...(threadKey ? { threadKey } : {}),
110
+ actorId,
111
+ receivedAt,
112
+ // Socket Mode still requires an ack within 3s, but the ack is now held
113
+ // until this event is durably recorded. Acking first would let a crash
114
+ // between ack and journal lose a message Slack considers delivered.
115
+ ackPolicy: 'after-durable-append',
116
+ ...(envelopeId ? { rawEnvelopeRef: rawRef('slack:envelope', envelopeId) } : {}),
117
+ target: input.target,
118
+ };
119
+ }
120
+ /**
121
+ * Discord: `receivedAt` comes from the adapter clock for the same reason as
122
+ * Telegram. discord.js exposes `msg.createdTimestamp`, but this tree extracts it
123
+ * nowhere, and it is derived from the snowflake's CREATION time — for an edited or
124
+ * gateway-replayed message that can sit far from when we received it. Rather than
125
+ * guess which one the ingress journal means, record when we observed it.
126
+ */
127
+ export function discordInboundEnvelope(input) {
128
+ const accountId = (input.botUserId ?? '').trim();
129
+ const eventId = (input.messageId ?? '').trim();
130
+ const channelId = (input.channelId ?? '').trim();
131
+ const actorId = (input.authorId ?? '').trim();
132
+ if (!accountId || !eventId || !channelId || !actorId)
133
+ return null;
134
+ if (input.target.channel !== 'discord')
135
+ return null;
136
+ // A DM has no guild. 'dm' occupies the guild slot so a DM conversation key can
137
+ // never collide with a guild id, and stays readable in a log line.
138
+ const guildId = (input.guildId ?? '').trim() || 'dm';
139
+ // In a thread the channel id IS the thread id, so the parent channel identifies
140
+ // the conversation (mirrors `buildDiscordTarget`).
141
+ const parentId = (input.parentId ?? '').trim();
142
+ const conversationId = input.isThread ? (parentId || channelId) : channelId;
143
+ return {
144
+ channel: 'discord',
145
+ accountId,
146
+ eventId,
147
+ conversationKey: `discord:${guildId}:${conversationId}`,
148
+ ...(input.isThread ? { threadKey: channelId } : {}),
149
+ actorId,
150
+ receivedAt: (input.now ?? Date.now)(),
151
+ // The gateway acks on its own schedule, outside this process's control.
152
+ ackPolicy: 'transport-managed',
153
+ rawEnvelopeRef: rawRef('discord:message', eventId),
154
+ target: input.target,
155
+ };
156
+ }
157
+ /** Telegram ids arrive as numbers on the wire and as strings everywhere in this
158
+ * tree. Rejects the empty string and non-finite numbers so a bad id drops the
159
+ * event instead of producing `'NaN'` or `'undefined'` as a key fragment. */
160
+ function idString(value) {
161
+ if (value == null)
162
+ return null;
163
+ if (typeof value === 'number')
164
+ return Number.isFinite(value) ? String(value) : null;
165
+ const trimmed = value.trim();
166
+ return trimmed ? trimmed : null;
167
+ }
168
+ //# sourceMappingURL=inbound-envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-envelope.js","sourceRoot":"","sources":["../../../src/messaging/inbound-envelope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ // ─── Ingress replay audit trail ──────────────────────
2
+ // A replay re-runs somebody's message. Who asked, why, and what state it was in
3
+ // beforehand are the facts you want when the same message turns up twice and nobody
4
+ // remembers touching it — so they are written before anyone can be asked.
5
+ //
6
+ // Append-only JSONL rather than a table: this is evidence, and a file that is only
7
+ // ever appended to is harder to quietly revise than a row that can be updated.
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { JAW_HOME } from '../core/config.js';
11
+ export function ingressAuditPath() {
12
+ return path.join(JAW_HOME, 'messaging-ingress-audit.jsonl');
13
+ }
14
+ export function appendIngressAudit(entry) {
15
+ const file = ingressAuditPath();
16
+ fs.mkdirSync(path.dirname(file), { recursive: true });
17
+ fs.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8');
18
+ }
19
+ /** Most recent first. A malformed line is skipped rather than failing the read: a
20
+ * partially written tail must not hide the history before it. */
21
+ export function readIngressAudit(limit = 20) {
22
+ const file = ingressAuditPath();
23
+ let raw;
24
+ try {
25
+ raw = fs.readFileSync(file, 'utf8');
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ const entries = [];
31
+ for (const line of raw.split('\n')) {
32
+ if (!line.trim())
33
+ continue;
34
+ try {
35
+ entries.push(JSON.parse(line));
36
+ }
37
+ catch { /* skip a torn line, keep the rest */ }
38
+ }
39
+ return entries.reverse().slice(0, limit);
40
+ }
41
+ //# sourceMappingURL=ingress-audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingress-audit.js","sourceRoot":"","sources":["../../../src/messaging/ingress-audit.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,gFAAgF;AAChF,oFAAoF;AACpF,0EAA0E;AAC1E,EAAE;AACF,mFAAmF;AACnF,+EAA+E;AAE/E,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAc7C,MAAM,UAAU,gBAAgB;IAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,+BAA+B,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAwB;IACvD,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAClE,CAAC;AAED;kEACkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,KAAK,GAAG,EAAE;IACvC,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QAAC,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAAC,CAAC;IAC5C,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;IACpB,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAsB,CAAC,CAAC;QAAC,CAAC;QAC5D,MAAM,CAAC,CAAC,qCAAqC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,22 @@
1
+ // Current chat_sessions.generation for an inbound envelope.
2
+ //
3
+ // Envelope.conversationKey is a routing key (`telegram:123`), not
4
+ // remote_session_bindings.remote_key (`jaw:telegram:...`). Look up through
5
+ // buildRemoteBindingKey(target). Missing binding or an uninitialized
6
+ // session-generation store is generation 0 — same as a brand-new session.
7
+ import { buildRemoteBindingKey } from './session-key.js';
8
+ import { getRemoteBoundSessionId } from '../core/chat-sessions.js';
9
+ import { readSessionGeneration } from '../core/session-generation.js';
10
+ export function currentGenerationForEnvelope(envelope) {
11
+ try {
12
+ const remoteKey = buildRemoteBindingKey(envelope.target);
13
+ const sessionId = getRemoteBoundSessionId(remoteKey);
14
+ if (!sessionId)
15
+ return 0;
16
+ return readSessionGeneration({ chatSessionId: sessionId, conversationKey: remoteKey });
17
+ }
18
+ catch {
19
+ return 0;
20
+ }
21
+ }
22
+ //# sourceMappingURL=ingress-generation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingress-generation.js","sourceRoot":"","sources":["../../../src/messaging/ingress-generation.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,kEAAkE;AAClE,2EAA2E;AAC3E,qEAAqE;AACrE,0EAA0E;AAE1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,MAAM,UAAU,4BAA4B,CAAC,QAAyB;IAClE,IAAI,CAAC;QACD,MAAM,SAAS,GAAG,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACzD,MAAM,SAAS,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,CAAC,SAAS;YAAE,OAAO,CAAC,CAAC;QACzB,OAAO,qBAAqB,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC,CAAC;IAC3F,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,CAAC,CAAC;IACb,CAAC;AACL,CAAC"}
@@ -0,0 +1,69 @@
1
+ // In-process messaging metrics.
2
+ //
3
+ // Labels are a closed set. Actor, event, conversation, and session ids are
4
+ // high-cardinality and belong on the trace, not here. There is no exporter
5
+ // in this cycle — snapshot() is what health/doctor read.
6
+ export const METRIC_LABEL_KEYS = ['channel', 'state', 'result'];
7
+ const ALLOWED = new Set(METRIC_LABEL_KEYS);
8
+ const counters = new Map();
9
+ const histograms = new Map();
10
+ function sanitizeLabels(labels) {
11
+ const out = {};
12
+ if (!labels)
13
+ return out;
14
+ for (const key of METRIC_LABEL_KEYS) {
15
+ const value = labels[key];
16
+ if (typeof value === 'string' && value.length > 0)
17
+ out[key] = value;
18
+ }
19
+ return out;
20
+ }
21
+ function seriesKey(name, labels) {
22
+ const parts = [name];
23
+ for (const key of METRIC_LABEL_KEYS) {
24
+ if (labels[key])
25
+ parts.push(`${key}=${labels[key]}`);
26
+ }
27
+ return parts.join('|');
28
+ }
29
+ export function inc(name, labels = {}, by = 1) {
30
+ if (!Number.isFinite(by) || by === 0)
31
+ return;
32
+ const safe = sanitizeLabels(labels);
33
+ const key = seriesKey(name, safe);
34
+ const existing = counters.get(key);
35
+ if (existing) {
36
+ existing.value += by;
37
+ return;
38
+ }
39
+ counters.set(key, { name, labels: safe, value: by });
40
+ }
41
+ export function observe(name, value, labels = {}) {
42
+ if (!Number.isFinite(value))
43
+ return;
44
+ const safe = sanitizeLabels(labels);
45
+ const key = seriesKey(name, safe);
46
+ const existing = histograms.get(key);
47
+ if (existing) {
48
+ existing.count += 1;
49
+ existing.sum += value;
50
+ existing.min = Math.min(existing.min, value);
51
+ existing.max = Math.max(existing.max, value);
52
+ return;
53
+ }
54
+ histograms.set(key, { name, labels: safe, count: 1, sum: value, min: value, max: value });
55
+ }
56
+ export function snapshotMetrics() {
57
+ return {
58
+ counters: [...counters.values()].map((row) => ({ ...row, labels: { ...row.labels } })),
59
+ histograms: [...histograms.values()].map((row) => ({ ...row, labels: { ...row.labels } })),
60
+ };
61
+ }
62
+ /** Test seam: the registry is process-global. */
63
+ export function __resetMessagingMetricsForTests() {
64
+ counters.clear();
65
+ histograms.clear();
66
+ }
67
+ // Keep the unused set referenced so a future label cannot be added silently.
68
+ void ALLOWED;
69
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/messaging/metrics.ts"],"names":[],"mappings":"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"}