cli-jaw 2.2.19 → 2.2.20

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 (95) hide show
  1. package/README.md +94 -3
  2. package/dist/server.js +11 -0
  3. package/dist/server.js.map +1 -1
  4. package/dist/src/agent/spawn/process-kill.js +96 -2
  5. package/dist/src/agent/spawn/process-kill.js.map +1 -1
  6. package/dist/src/agent/spawn.js +34 -65
  7. package/dist/src/agent/spawn.js.map +1 -1
  8. package/dist/src/bgtask/runner.js +26 -21
  9. package/dist/src/bgtask/runner.js.map +1 -1
  10. package/dist/src/cli/acp-client.js +11 -5
  11. package/dist/src/cli/acp-client.js.map +1 -1
  12. package/dist/src/cli/employee-handler.js +3 -2
  13. package/dist/src/cli/employee-handler.js.map +1 -1
  14. package/dist/src/code-mode/acp-host.js +22 -10
  15. package/dist/src/code-mode/acp-host.js.map +1 -1
  16. package/dist/src/core/config.js +14 -0
  17. package/dist/src/core/config.js.map +1 -1
  18. package/dist/src/core/db.js +16 -0
  19. package/dist/src/core/db.js.map +1 -1
  20. package/dist/src/core/employees.js +22 -4
  21. package/dist/src/core/employees.js.map +1 -1
  22. package/dist/src/core/windows-shell.js +63 -0
  23. package/dist/src/core/windows-shell.js.map +1 -0
  24. package/dist/src/discord/bot.js +199 -147
  25. package/dist/src/discord/bot.js.map +1 -1
  26. package/dist/src/discord/gateway-supervisor.js +303 -0
  27. package/dist/src/discord/gateway-supervisor.js.map +1 -0
  28. package/dist/src/discord/rest-scheduler.js +448 -0
  29. package/dist/src/discord/rest-scheduler.js.map +1 -0
  30. package/dist/src/discord/send-only-client.js +59 -43
  31. package/dist/src/discord/send-only-client.js.map +1 -1
  32. package/dist/src/manager/lifecycle.js +7 -2
  33. package/dist/src/manager/lifecycle.js.map +1 -1
  34. package/dist/src/manager/notes/capabilities.js +9 -7
  35. package/dist/src/manager/notes/capabilities.js.map +1 -1
  36. package/dist/src/memory/bootstrap.js +5 -1
  37. package/dist/src/memory/bootstrap.js.map +1 -1
  38. package/dist/src/memory/host-toolchain.js +156 -0
  39. package/dist/src/memory/host-toolchain.js.map +1 -0
  40. package/dist/src/messaging/channel-capabilities.js +30 -0
  41. package/dist/src/messaging/channel-capabilities.js.map +1 -0
  42. package/dist/src/messaging/delivery-outcome.js +73 -0
  43. package/dist/src/messaging/delivery-outcome.js.map +1 -0
  44. package/dist/src/messaging/draft-stream.js +154 -0
  45. package/dist/src/messaging/draft-stream.js.map +1 -0
  46. package/dist/src/messaging/runtime.js +3 -9
  47. package/dist/src/messaging/runtime.js.map +1 -1
  48. package/dist/src/messaging/send.js +40 -8
  49. package/dist/src/messaging/send.js.map +1 -1
  50. package/dist/src/messaging/types.js +22 -1
  51. package/dist/src/messaging/types.js.map +1 -1
  52. package/dist/src/notes/search.js +6 -4
  53. package/dist/src/notes/search.js.map +1 -1
  54. package/dist/src/prompt/builder.js +110 -6
  55. package/dist/src/prompt/builder.js.map +1 -1
  56. package/dist/src/prompt/conversation-context.js +23 -0
  57. package/dist/src/prompt/conversation-context.js.map +1 -0
  58. package/dist/src/prompt/templates/a1-system.md +28 -12
  59. package/dist/src/prompt/templates/control-system.md +19 -6
  60. package/dist/src/prompt/templates/employee.md +16 -3
  61. package/dist/src/security/path-guards.js +124 -27
  62. package/dist/src/security/path-guards.js.map +1 -1
  63. package/dist/src/slack/api.js +38 -5
  64. package/dist/src/slack/api.js.map +1 -1
  65. package/dist/src/slack/bot.js +214 -35
  66. package/dist/src/slack/bot.js.map +1 -1
  67. package/dist/src/slack/context.js +192 -0
  68. package/dist/src/slack/context.js.map +1 -0
  69. package/dist/src/slack/conversation.js +275 -0
  70. package/dist/src/slack/conversation.js.map +1 -0
  71. package/dist/src/slack/enrichment-cache.js +334 -0
  72. package/dist/src/slack/enrichment-cache.js.map +1 -0
  73. package/dist/src/slack/history.js +47 -8
  74. package/dist/src/slack/history.js.map +1 -1
  75. package/dist/src/slack/identity.js +99 -190
  76. package/dist/src/slack/identity.js.map +1 -1
  77. package/dist/src/slack/ingress.js +78 -2
  78. package/dist/src/slack/ingress.js.map +1 -1
  79. package/dist/src/slack/progress.js +39 -51
  80. package/dist/src/slack/progress.js.map +1 -1
  81. package/dist/src/slack/thread-tracker.js +78 -0
  82. package/dist/src/slack/thread-tracker.js.map +1 -1
  83. package/dist/src/telegram/bot.js +105 -56
  84. package/dist/src/telegram/bot.js.map +1 -1
  85. package/dist/src/telegram/update-offset.js +152 -0
  86. package/dist/src/telegram/update-offset.js.map +1 -0
  87. package/package.json +1 -1
  88. package/public/dist/assets/Agent-C5EvX-_v.js +1 -0
  89. package/public/dist/assets/{manager-Bjuh1jpq.js → manager-CP-Au860.js} +1 -1
  90. package/public/dist/manager/index.html +1 -1
  91. package/public/manager/src/settings/cli-status-polling.ts +84 -0
  92. package/public/manager/src/settings/pages/Agent.tsx +84 -5
  93. package/scripts/install-officecli.ps1 +38 -2
  94. package/scripts/install.ps1 +1 -1
  95. package/public/dist/assets/Agent-nMPZsMfN.js +0 -1
@@ -12,6 +12,80 @@ function badRequest(code) {
12
12
  function forbidden(code) {
13
13
  return Object.assign(new Error(code), { statusCode: 403 });
14
14
  }
15
+ function defaultRealpath(p) {
16
+ try {
17
+ return fs.realpathSync.native(p);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export const hostPathEnvironment = {
24
+ impl: path,
25
+ windows: process.platform === 'win32',
26
+ realpath: defaultRealpath,
27
+ resolveHome: resolveHomePath,
28
+ };
29
+ /**
30
+ * Reject NTFS alternate-data-stream suffixes and Win32-trimmed components.
31
+ *
32
+ * Measured on Windows (devlog/_plan/260812_windows_and_channels_parity/005):
33
+ * writing `a.md:hidden` succeeds, the stream is absent from a name-only
34
+ * directory listing, and its content is still fully readable — so a name-based
35
+ * allowlist never sees the payload. `trailing.md.` lands on disk as
36
+ * `trailing.md`, so two distinct strings name one file.
37
+ *
38
+ * Only applied under Windows semantics: on POSIX a colon is a legal filename
39
+ * character, and rejecting it there would break working callers.
40
+ */
41
+ export function assertNoWindowsStreamSuffix(input, env = hostPathEnvironment) {
42
+ if (!env.windows)
43
+ return;
44
+ const value = String(input || '');
45
+ // Strip the ROOT before scanning. `path.win32.parse` understands drive
46
+ // roots (`C:\`), UNC roots (`\\server\share\`), and extended-length
47
+ // namespace roots (`\\?\C:\`, `\\?\UNC\server\share\`). Scanning the whole
48
+ // string instead would reject `\\?\C:\Data\f.md` — a legitimate long-path
49
+ // form that `path.toNamespacedPath()` emits — as if its drive colon were a
50
+ // stream separator.
51
+ const root = path.win32.parse(value).root;
52
+ const tail = root ? value.slice(root.length) : value;
53
+ for (const seg of tail.split(/[\\/]+/)) {
54
+ if (!seg)
55
+ continue;
56
+ // No colon is legal outside the root: this is the NTFS ADS separator.
57
+ if (seg.includes(':'))
58
+ throw forbidden('path_stream_denied');
59
+ // `.` and `..` are ordinary relative segments that legitimately end in
60
+ // a dot. Rejecting them here would mask traversal as an encoding error
61
+ // and break every relative path — containment, not this rule, is what
62
+ // decides whether `..` is allowed to escape.
63
+ if (seg === '.' || seg === '..')
64
+ continue;
65
+ if (/[ .]$/.test(seg))
66
+ throw forbidden('path_trailing_trim_denied');
67
+ }
68
+ }
69
+ /**
70
+ * Canonical identity for containment comparison.
71
+ *
72
+ * Deliberately NOT case-folding, on any platform. Folding ASCII merely because
73
+ * the host is Windows is an over-authorization bug, not a convenience: Windows
74
+ * supports per-directory case sensitivity (`fsutil file setCaseSensitiveInfo`)
75
+ * and case-sensitive SMB shares, so `...\Root` and `...\root` can be two
76
+ * different directories. Folding makes a forbidden sibling look contained.
77
+ * This was demonstrated on a real Windows host — see
78
+ * devlog/_plan/260812_windows_and_channels_parity/010.
79
+ *
80
+ * Case-insensitivity is instead obtained where it is actually true: both the
81
+ * candidate and the roots in `assertSendFilePath` pass through native
82
+ * `realpath`, which restores each entry's real on-disk casing. Comparing those
83
+ * canonical forms exactly is both correct on case-insensitive volumes and safe
84
+ * on case-sensitive ones.
85
+ */
86
+ export function pathIdentity(p) {
87
+ return p;
88
+ }
15
89
  /**
16
90
  * Skill ID 검증 — 소문자 영숫자 + 하이픈/점/밑줄만 허용
17
91
  * @param {string} id
@@ -80,12 +154,19 @@ export function assertMemoryRelPath(input, { allowExt = ['.md'] } = {}) {
80
154
  * @returns {string} resolved absolute path
81
155
  * @throws 403 path_escape
82
156
  */
83
- export function safeResolveUnder(baseDir, unsafeName) {
84
- const base = path.resolve(baseDir);
85
- const resolved = path.resolve(base, unsafeName);
86
- const pref = base.endsWith(path.sep) ? base : base + path.sep;
87
- if (resolved !== base && !resolved.startsWith(pref))
157
+ export function safeResolveUnder(baseDir, unsafeName, env = hostPathEnvironment) {
158
+ assertNoWindowsStreamSuffix(unsafeName, env);
159
+ const p = env.impl;
160
+ const base = p.resolve(baseDir);
161
+ const resolved = p.resolve(base, unsafeName);
162
+ // Exact comparison, deliberately. This helper is purely lexical — it never
163
+ // canonicalizes — so it cannot know whether the volume is case sensitive.
164
+ // Folding here would be a guess that fails open on a case-sensitive
165
+ // directory; exact matching only ever fails closed.
166
+ const pref = base.endsWith(p.sep) ? base : base + p.sep;
167
+ if (resolved !== base && !resolved.startsWith(pref)) {
88
168
  throw forbidden('path_escape');
169
+ }
89
170
  return resolved;
90
171
  }
91
172
  /**
@@ -93,39 +174,55 @@ export function safeResolveUnder(baseDir, unsafeName) {
93
174
  * Prevents arbitrary file exfiltration via /api/telegram/send, /api/channel/send, etc.
94
175
  * @throws 403 path_not_allowed
95
176
  */
96
- function safeRealpath(p) {
97
- try {
98
- return fs.realpathSync.native(p);
99
- }
100
- catch {
101
- return null;
102
- }
103
- }
104
- function isUnderRoot(canonical, root) {
105
- const pref = root.endsWith(path.sep) ? root : root + path.sep;
106
- return canonical === root || canonical.startsWith(pref);
177
+ /**
178
+ * Containment between two ALREADY-CANONICAL paths.
179
+ *
180
+ * Both sides come from native `realpath`, so each carries its true on-disk
181
+ * casing and an exact comparison is right on case-insensitive and
182
+ * case-sensitive volumes alike.
183
+ */
184
+ function isUnderRoot(canonical, root, env = hostPathEnvironment) {
185
+ const c = pathIdentity(canonical);
186
+ const r = pathIdentity(root);
187
+ const pref = r.endsWith(env.impl.sep) ? r : r + env.impl.sep;
188
+ return c === r || c.startsWith(pref);
107
189
  }
108
- export function assertSendFilePath(filePath, workingDir, projectDirs) {
109
- const resolved = path.resolve(filePath);
110
- const canonical = safeRealpath(resolved);
190
+ export function assertSendFilePath(filePath, workingDir, projectDirs, env = hostPathEnvironment) {
191
+ // Reject stream/trim forms before any filesystem call: `a.md:hidden`
192
+ // resolves and realpaths cleanly, so a later check would already be too late.
193
+ assertNoWindowsStreamSuffix(filePath, env);
194
+ const p = env.impl;
195
+ const resolved = p.resolve(filePath);
196
+ const canonical = env.realpath(resolved);
111
197
  if (!canonical)
112
198
  throw forbidden('path_not_resolvable');
113
199
  // Allow anything under JAW_HOME
114
- const jawHome = resolveHomePath(process.env["CLI_JAW_HOME"] || process.env["JAW_HOME"] || path.join(os.homedir(), '.cli-jaw'));
115
- const canonJaw = safeRealpath(jawHome);
116
- if (canonJaw && isUnderRoot(canonical, canonJaw))
200
+ const jawHome = env.resolveHome(process.env["CLI_JAW_HOME"] || process.env["JAW_HOME"] || p.join(os.homedir(), '.cli-jaw'));
201
+ const canonJaw = env.realpath(jawHome);
202
+ if (canonJaw && isUnderRoot(canonical, canonJaw, env))
117
203
  return canonical;
118
204
  if (workingDir) {
119
- const canonWd = safeRealpath(path.resolve(workingDir));
120
- if (canonWd && isUnderRoot(canonical, canonWd))
205
+ const canonWd = env.realpath(p.resolve(workingDir));
206
+ if (canonWd && isUnderRoot(canonical, canonWd, env))
121
207
  return canonical;
122
208
  }
123
209
  if (projectDirs) {
124
210
  for (const dir of projectDirs) {
125
- const currentReal = safeRealpath(path.resolve(dir));
126
- if (!currentReal || currentReal !== path.resolve(dir))
211
+ // Compare canonical-to-canonical. The previous form required
212
+ // `realpath(dir) === resolve(dir)`, which silently dropped every
213
+ // project root on Windows whose stored casing differed from the
214
+ // on-disk casing, because native realpath restores real casing
215
+ // (verified on a Windows host: input `...\mixed` canonicalizes to
216
+ // `...\MiXeD`, so the equality never held).
217
+ //
218
+ // Resolving the root is also what makes containment meaningful: the
219
+ // candidate is already canonical, so both sides must be. A root
220
+ // that is a symlink is therefore evaluated at its target, which is
221
+ // the location the operator actually granted by configuring it.
222
+ const currentReal = env.realpath(p.resolve(dir));
223
+ if (!currentReal)
127
224
  continue;
128
- if (isUnderRoot(canonical, currentReal))
225
+ if (isUnderRoot(canonical, currentReal, env))
129
226
  return canonical;
130
227
  }
131
228
  }
@@ -1 +1 @@
1
- {"version":3,"file":"path-guards.js","sourceRoot":"","sources":["../../../src/security/path-guards.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"path-guards.js","sourceRoot":"","sources":["../../../src/security/path-guards.ts"],"names":[],"mappings":"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"}
@@ -87,8 +87,22 @@ export const redactSlackTokens = redactChannelSecrets;
87
87
  * The repo runs `exactOptionalPropertyTypes`, so `{ status: undefined }` is not
88
88
  * assignable to `{ status?: number }` — the key has to be absent instead.
89
89
  */
90
- export function slackFailure(error, status) {
91
- return status === undefined ? { ok: false, error } : { ok: false, error, status };
90
+ export function slackFailure(error, status, retryAfterMs) {
91
+ return {
92
+ ok: false,
93
+ error,
94
+ ...(status !== undefined ? { status } : {}),
95
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
96
+ };
97
+ }
98
+ export function parseRetryAfterMs(headers) {
99
+ const raw = headers.get('retry-after');
100
+ if (raw == null || raw.trim() === '')
101
+ return undefined;
102
+ const seconds = Number(raw);
103
+ if (!Number.isFinite(seconds) || seconds < 0)
104
+ return undefined;
105
+ return Math.ceil(seconds * 1000);
92
106
  }
93
107
  /**
94
108
  * Call a Slack Web API method.
@@ -127,21 +141,40 @@ export async function slackApi(token, method, body, options = {}) {
127
141
  }
128
142
  try {
129
143
  const response = await doFetch(url, init);
144
+ const retryAfterMs = response.headers
145
+ ? parseRetryAfterMs(response.headers)
146
+ : undefined;
130
147
  const text = await response.text();
131
148
  let parsed = {};
132
149
  try {
133
150
  parsed = text ? JSON.parse(text) : {};
134
151
  }
135
152
  catch {
136
- return { ok: false, error: 'invalid_json_response', status: response.status };
153
+ return {
154
+ ok: false,
155
+ error: 'invalid_json_response',
156
+ status: response.status,
157
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
158
+ };
137
159
  }
138
160
  // Slack signals application errors with HTTP 200 + ok:false.
139
161
  if (parsed['ok'] !== true) {
140
162
  const err = typeof parsed['error'] === 'string' ? parsed['error'] : 'unknown_error';
141
163
  log.warn('[slack:api]', redactSlackTokens(`${method} failed: ${err}`));
142
- return { ok: false, error: err, status: response.status, data: parsed };
164
+ return {
165
+ ok: false,
166
+ error: err,
167
+ status: response.status,
168
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
169
+ data: parsed,
170
+ };
143
171
  }
144
- return { ok: true, status: response.status, data: parsed };
172
+ return {
173
+ ok: true,
174
+ status: response.status,
175
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
176
+ data: parsed,
177
+ };
145
178
  }
146
179
  catch (error) {
147
180
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/slack/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/slack/api.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,0EAA0E;AAC1E,2EAA2E;AAC3E,0EAA0E;AAC1E,EAAE;AACF,gEAAgE;AAChE,6EAA6E;AAC7E,mEAAmE;AAEnE,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAU/C,qEAAqE;AACrE,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC;IACnC,aAAa;IACb,qBAAqB;IACrB,gBAAgB;IAChB,iBAAiB;IACjB,aAAa;CAChB,CAAC,CAAC;AAEH,MAAM,UAAU,qBAAqB,CAAC,KAAyB;IAC3D,OAAO,CAAC,CAAC,KAAK,IAAI,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,gEAAgE;AAChE;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAa;IACzC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACjD,MAAM,MAAM,GAAG,IAA+B,CAAC;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACtE,MAAM,IAAI,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACzC,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAI,IAAgC,CAAC,UAAU,CAAC,CAAC;QAC/D,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7E,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;QACnD,CAAC;IACL,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAyB,EAAE,IAAc;IACxE,QAAQ,KAAK,EAAE,CAAC;QACZ,KAAK,cAAc,CAAC;QACpB,KAAK,YAAY;YACb,OAAO,mEAAmE,CAAC;QAC/E,KAAK,kBAAkB;YACnB,OAAO,2DAA2D,CAAC;QACvE,KAAK,eAAe,CAAC,CAAC,CAAC;YACnB,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACrC,OAAO,MAAM;gBACT,CAAC,CAAC,6BAA6B,MAAM,0EAA0E;gBAC/G,CAAC,CAAC,wGAAwG,CAAC;QACnH,CAAC;QACD,KAAK,mBAAmB;YACpB,OAAO,gEAAgE,CAAC;QAC5E,KAAK,gBAAgB;YACjB,OAAO,mEAAmE,CAAC;QAC/E,KAAK,aAAa;YACd,OAAO,gCAAgC,CAAC;QAC5C,KAAK,cAAc;YACf,OAAO,wDAAwD,CAAC;QACpE,KAAK,aAAa;YACd,OAAO,sCAAsC,CAAC;QAClD;YACI,OAAO,KAAK,CAAC,CAAC,CAAC,oBAAoB,KAAK,EAAE,CAAC,CAAC,CAAC,yBAAyB,CAAC;IAC/E,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AAItD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CACxB,KAAa,EACb,MAAe,EACf,YAAqB;IAErB,OAAO;QACH,EAAE,EAAE,KAAK;QACT,KAAK;QACL,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAC;AACN,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAA6B;IAC3D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACvC,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACvD,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/D,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC1B,KAAa,EACb,MAAc,EACd,IAA8B,EAC9B,UAAgG,EAAE;IAElG,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC3C,MAAM,GAAG,GAAG,GAAG,cAAc,IAAI,MAAM,EAAE,CAAC;IAC1C,MAAM,OAAO,GAA2B,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,CAAC;IAC7E,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IACtD,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACpD,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC5F,CAAC;SAAM,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACjC,CAAC;SAAM,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,CAAC,cAAc,CAAC,GAAG,kDAAkD,CAAC;QAC7E,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IAClC,CAAC;SAAM,IAAI,IAAI,EAAE,CAAC;QACd,OAAO,CAAC,cAAc,CAAC,GAAG,iCAAiC,CAAC;QAC5D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,IAAI,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1C,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO;YACjC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC;YACrC,CAAC,CAAC,SAAS,CAAC;QAChB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,MAAM,GAA4B,EAAE,CAAC;QACzC,IAAI,CAAC;YACD,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACL,OAAO;gBACH,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,uBAAuB;gBAC9B,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1D,CAAC;QACN,CAAC;QACD,6DAA6D;QAC7D,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC;YACpF,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,iBAAiB,CAAC,GAAG,MAAM,YAAY,GAAG,EAAE,CAAC,CAAC,CAAC;YACvE,OAAO;gBACH,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvD,IAAI,EAAE,MAAW;aACpB,CAAC;QACN,CAAC;QACD,OAAO;YACH,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,IAAI,EAAE,MAAW;SACpB,CAAC;IACN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO;YACH,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,iBAAiB,CAAE,KAAe,CAAC,OAAO,CAAC;YAClD,MAAM,EAAE,GAAG;SACd,CAAC;IACN,CAAC;AACL,CAAC"}
@@ -18,17 +18,23 @@ import { buildMediaPromptMany } from '../agent/spawn.js';
18
18
  import { slackApi } from './api.js';
19
19
  import { SlackSocketClient } from './socket.js';
20
20
  import { resolveEventText, shouldAttachSlack, shouldProcessSlackEvent } from './events.js';
21
- import { isThreadParticipated, markThreadParticipated } from './thread-tracker.js';
21
+ import { isThreadParticipated, markThreadParticipated, claimThreadPrefetch, commitThreadPrefetch, releaseThreadPrefetch, resetThreadPrefetchClaims, } from './thread-tracker.js';
22
22
  import { sendSlackText, getSlackSendClient } from './send-only-client.js';
23
23
  import { startSlackProgress, statusFromToolEvent } from './progress.js';
24
24
  import { createSlackForwarder, relaySlackImages } from './forwarder.js';
25
25
  import { handleSlackSlashCommand } from './commands.js';
26
26
  import { logErrorText, redactOutboundText } from '../messaging/redact.js';
27
27
  import { downloadAndSaveSlackFiles } from './inbound-file.js';
28
- import { admitSlackRun, claimSlackEvent, enqueueSlackIngress, resetSlackIngress, slackEventKey, slackIngressLaneKey } from './ingress.js';
28
+ import { admitSlackRun, claimSlackEvent, commitSlackEvent, currentIngressGeneration, enqueueSlackIngress, isIngressGenerationCurrent, resetSlackIngress, slackEventKey, slackIngressLaneKey } from './ingress.js';
29
29
  import { buildSenderDisplay, buildSenderPrompt, resolveSenderIdentity } from './identity.js';
30
+ import { resolveConversationInfo, resolveThreadInfo } from './conversation.js';
31
+ import { buildSlackContextBlock, applySlackContext, buildThreadPreamble, ROSTER_PREVIEW } from './context.js';
32
+ import { formatHistoryForAgent } from './history.js';
33
+ import { cachedNameMap } from './conversation.js';
34
+ import { fetchSlackChannelMembers } from './roster.js';
30
35
  import { recoverSlackAttachments } from './attachment-recovery.js';
31
36
  import { resetSlackIdentityCache } from './identity.js';
37
+ import { resetSlackConversationCache } from './conversation.js';
32
38
  let socketClient = null;
33
39
  let forwarderHandler = null;
34
40
  let selfUserId = null;
@@ -79,7 +85,7 @@ function buildSlackTarget(event) {
79
85
  });
80
86
  }
81
87
  // ─── Dispatch (full reply path) ─────────────────────
82
- async function slackOrchestrate(target, prompt, displayMsg, signal) {
88
+ async function slackOrchestrate(target, prompt, displayMsg, signal, dedupe = {}) {
83
89
  const client = getSlackSendClient();
84
90
  if (!client.token)
85
91
  return;
@@ -87,6 +93,15 @@ async function slackOrchestrate(target, prompt, displayMsg, signal) {
87
93
  const chatId = target.targetId;
88
94
  if (signal.aborted)
89
95
  return;
96
+ // #321: the reservation was taken before an `await` and several early
97
+ // returns. If a reset landed in that window this delivery belongs to a dead
98
+ // generation — a redelivery has already re-reserved it, and admitting here
99
+ // would run the same message twice.
100
+ if (dedupe.reservationGeneration !== undefined
101
+ && !isIngressGenerationCurrent(dedupe.reservationGeneration)) {
102
+ log.info('[slack:in] skipped (stale_generation)');
103
+ return;
104
+ }
90
105
  const result = admitSlackRun({
91
106
  target, prompt, displayText: displayMsg, chatId,
92
107
  runReply: async (ctx) => {
@@ -128,6 +143,11 @@ async function slackOrchestrate(target, prompt, displayMsg, signal) {
128
143
  }
129
144
  },
130
145
  });
146
+ // Durable commit AFTER admission, with no await in between: an event that
147
+ // died before this line stays redeliverable, which is the whole point of
148
+ // ordering it here rather than at reservation time.
149
+ if (dedupe.eventKey && result.action !== 'rejected')
150
+ commitSlackEvent(dedupe.eventKey);
131
151
  result.laneTail?.catch(error => log.error('[slack:lane]', logErrorText(error)));
132
152
  if (result.action === 'queued') {
133
153
  log.info(`[slack:queue] agent busy, queued (${result.pending} pending)`);
@@ -185,7 +205,26 @@ function buildSlackFileFailureWarning(failed, allFailed = false) {
185
205
  const items = failed.map(file => `- ${file.name}: ${t(`slack.files.error.${file.code}`, {}, locale)}`);
186
206
  return `${t(allFailed ? 'slack.files.allFailure' : 'slack.files.partialFailure', {}, locale)}\n${items.join('\n')}`;
187
207
  }
188
- export async function processSlackMessageEvent(event, target, text, signal) {
208
+ export async function processSlackMessageEvent(event, target, text, signal, opts = {}) {
209
+ // The claim was taken synchronously in handleSlackEnvelope, before this task
210
+ // was queued. Every path out of here that did NOT inject history has to give
211
+ // it back, or one skipped attempt silences the thread for the whole runtime.
212
+ // The paths are many (empty prompt, all attachments failed, abort, continue
213
+ // intent, config off, lookup failure, deadline), so the release is a finally
214
+ // rather than a return-by-return audit.
215
+ let prefetchCommitted = false;
216
+ try {
217
+ await runSlackMessageEvent(event, target, text, signal, opts, () => {
218
+ prefetchCommitted = Boolean(opts.prefetchToken) && commitThreadPrefetch(event.channel || '', event.thread_ts || '', opts.prefetchToken || 0);
219
+ });
220
+ }
221
+ finally {
222
+ if (opts.prefetchToken && !prefetchCommitted) {
223
+ releaseThreadPrefetch(event.channel || '', event.thread_ts || '', opts.prefetchToken);
224
+ }
225
+ }
226
+ }
227
+ async function runSlackMessageEvent(event, target, text, signal, opts, commitPrefetch) {
189
228
  const files = event.files || [];
190
229
  let prompt = text;
191
230
  let displayText = text;
@@ -225,10 +264,109 @@ export async function processSlackMessageEvent(event, target, text, signal) {
225
264
  // continuation, so control text travels undecorated. Reset is already
226
265
  // intercepted upstream and never reaches here.
227
266
  if (!isContinueIntent(prompt)) {
228
- prompt = buildSenderPrompt(identity, prompt);
267
+ const block = await buildInboundContextBlock(event, identity, signal, opts, commitPrefetch);
268
+ // An empty block lands EXACTLY on the previous behavior: config off and
269
+ // total lookup failure must be indistinguishable from before this
270
+ // feature existed.
271
+ prompt = block
272
+ ? applySlackContext(block, prompt)
273
+ : buildSenderPrompt(identity, prompt);
274
+ // Display text is unchanged either way: the UI/DB bubble shows who sent
275
+ // the message, and the conversation is already obvious in Slack's own UI.
229
276
  displayText = buildSenderDisplay(identity, displayText);
230
277
  }
231
- await slackOrchestrate(target, prompt, displayText, signal);
278
+ await slackOrchestrate(target, prompt, displayText, signal, {
279
+ ...(opts.eventKey ? { eventKey: opts.eventKey } : {}),
280
+ ...(opts.reservationGeneration !== undefined
281
+ ? { reservationGeneration: opts.reservationGeneration } : {}),
282
+ });
283
+ }
284
+ /**
285
+ * Inbound deadline for the whole context phase.
286
+ *
287
+ * Larger than identity's 400ms because this is up to two round trips, but still
288
+ * a hard bound: naming a conversation must never hold a user's message. Work
289
+ * that outlives the deadline keeps running and warms the cache, so the next
290
+ * message in the same conversation gets the full block.
291
+ */
292
+ const INBOUND_CONTEXT_DEADLINE_MS = 700;
293
+ async function buildInboundContextBlock(event, identity, signal, opts = {}, commitPrefetch = () => { }) {
294
+ const channel = event.channel || '';
295
+ // The caller's finally releases an uncommitted claim, so early returns here
296
+ // need no cleanup of their own.
297
+ if (settings["slack"]?.conversationContext === false)
298
+ return '';
299
+ if (!channel)
300
+ return '';
301
+ const token = getSlackSendClient().token;
302
+ if (!token)
303
+ return '';
304
+ const teamId = String(settings["slack"]?.teamId || 'unknown');
305
+ const threadTs = event.thread_ts || '';
306
+ const work = (async () => {
307
+ // Independent lookups: serial would double the round trips inside a
308
+ // deadline that exists to stay small.
309
+ const [conversation, thread] = await Promise.all([
310
+ resolveConversationInfo(token, channel, { teamId, signal }),
311
+ threadTs
312
+ ? resolveThreadInfo(token, channel, threadTs, { teamId, signal })
313
+ : Promise.resolve(undefined),
314
+ ]);
315
+ const roster = await resolveRosterContext(token, channel, teamId, conversation.kind, signal);
316
+ const block = buildSlackContextBlock({
317
+ identity,
318
+ conversation,
319
+ ...(thread ? { thread } : {}),
320
+ ...(roster ? { roster } : {}),
321
+ selfUserId,
322
+ });
323
+ // First entry into a thread already in progress: give the agent what was
324
+ // said before it was pulled in. Once only — later messages ride the
325
+ // agent session, and re-injecting would waste tokens and Tier 3 budget.
326
+ if (!opts.prefetchToken || !thread?.resolved || !thread.messages?.length)
327
+ return block;
328
+ // The current message is already the prompt body; repeating it here
329
+ // would show the agent its own input twice.
330
+ const prior = thread.messages.filter(message => message.ts !== event.ts);
331
+ if (!prior.length)
332
+ return block;
333
+ const authorIds = prior
334
+ .map(message => message.user || message.botId || '')
335
+ .filter(Boolean);
336
+ const preamble = buildThreadPreamble(formatHistoryForAgent(prior, selfUserId, cachedNameMap(teamId, authorIds)), thread.replyCount);
337
+ if (!preamble)
338
+ return block;
339
+ // History is actually going into the prompt: the claim is spent.
340
+ commitPrefetch();
341
+ return block ? `${block}\n${preamble}` : preamble;
342
+ })();
343
+ return raceContextDeadline(work, INBOUND_CONTEXT_DEADLINE_MS);
344
+ }
345
+ /** Opt-in channel roster. Off by default: see 021 contract §설정. */
346
+ async function resolveRosterContext(token, channel, teamId, kind, signal) {
347
+ if (settings["slack"]?.channelRoster !== true)
348
+ return undefined;
349
+ // In a DM the other party is the sender; a roster line would just repeat it.
350
+ if (kind === 'dm')
351
+ return undefined;
352
+ const result = await fetchSlackChannelMembers(token, channel, { teamId, signal, limit: 200 });
353
+ if (!result.ok)
354
+ return undefined;
355
+ const humans = result.members.filter(member => !member.isBot);
356
+ return {
357
+ names: humans.slice(0, ROSTER_PREVIEW).map(member => member.name),
358
+ total: humans.length,
359
+ // The walk is page-bounded, so a truncated result is a lower bound.
360
+ ...(result.hasMore ? { approximate: true } : {}),
361
+ };
362
+ }
363
+ function raceContextDeadline(work, ms) {
364
+ return new Promise(resolve => {
365
+ const timer = setTimeout(() => resolve(''), ms);
366
+ // unref: a pending deadline must never hold the process open.
367
+ timer.unref?.();
368
+ void work.then(value => { clearTimeout(timer); resolve(value); }, () => { clearTimeout(timer); resolve(''); });
369
+ });
232
370
  }
233
371
  // ─── Envelope routing ───────────────────────────────
234
372
  export async function handleSlackEnvelope(envelope) {
@@ -251,6 +389,10 @@ export async function handleSlackEnvelope(envelope) {
251
389
  log.info(`[slack:in] skipped (${decision.reason})`);
252
390
  return;
253
391
  }
392
+ // Carried to the admission site so the durable commit happens only after a
393
+ // run is accepted, and so a reset in between invalidates this delivery.
394
+ let reservedEventKey;
395
+ let reservationGeneration;
254
396
  // Message-level dedupe goes HERE — after the gate, before identity resolution.
255
397
  // After the gate, because Slack delivers a `message` copy and an `app_mention`
256
398
  // copy of one mention under the same ts; the gate drops the former, and letting
@@ -264,46 +406,77 @@ export async function handleSlackEnvelope(envelope) {
264
406
  log.info('[slack:in] skipped (duplicate_event)');
265
407
  return;
266
408
  }
409
+ reservedEventKey = eventKey;
410
+ reservationGeneration = currentIngressGeneration();
267
411
  }
268
412
  if (event.type === 'app_mention' && event.channel) {
269
413
  // A mention inside a thread marks that thread; a top-level mention
270
414
  // marks the thread this message would parent (marking point a).
271
415
  markThreadParticipated(event.channel, event.thread_ts || event.ts || '');
416
+ if (!event.thread_ts && event.ts) {
417
+ // A thread WE start needs no history: the parent mention and our
418
+ // reply are already the session's own context. Spend the claim now
419
+ // so the first follow-up does not re-inject what the agent said.
420
+ const token = claimThreadPrefetch(event.channel, event.ts);
421
+ commitThreadPrefetch(event.channel, event.ts, token);
422
+ }
272
423
  }
273
- const target = buildSlackTarget(event);
274
- setLastActiveTarget('slack', target);
275
- setLatestSeenTarget('slack', target);
276
- const text = resolveEventText(event, selfUserId);
277
- let hasFiles = Boolean(event.files?.length);
278
- // app_mention 봉투에는 files 없고, 첨부를 가진 message 사본은
279
- // shouldProcessSlackEvent 에서 mention_via_app_mention 으로 드롭된다.
280
- // 그래서 멘션과 함께 올린 파일은 여기서 되찾지 않으면 영영 사라진다.
281
- if (!hasFiles && event.type === 'app_mention' && event.channel && event.ts) {
282
- const recoverToken = getSlackSendClient().token;
283
- if (recoverToken) {
284
- const recovered = await recoverSlackAttachments(recoverToken, event.channel, event.ts, event.thread_ts ? { threadTs: event.thread_ts } : {});
285
- if (recovered.length) {
286
- event.files = recovered;
287
- hasFiles = true;
288
- log.info(`[slack:recover] ${event.channel} ts=${event.ts}: ${recovered.length} attachment(s)`);
424
+ // Claim the one-time thread prefetch HERE, synchronously, before the ingress
425
+ // task is queued. Asking inside the queue task instead would be a dead
426
+ // branch for app_mention (the mark above already ran) and a race for
427
+ // DM/listen-all channels (they are marked only after a successful reply,
428
+ // which is not awaited). On the accepted message-event path no `await` runs
429
+ // between this function's entry and this line, so the test-and-set is atomic
430
+ // against a second envelope in the same tick.
431
+ const prefetchToken = event.thread_ts
432
+ ? claimThreadPrefetch(event.channel || '', event.thread_ts)
433
+ : 0;
434
+ let prefetchHandedOff = false;
435
+ try {
436
+ const target = buildSlackTarget(event);
437
+ setLastActiveTarget('slack', target);
438
+ setLatestSeenTarget('slack', target);
439
+ const text = resolveEventText(event, selfUserId);
440
+ let hasFiles = Boolean(event.files?.length);
441
+ // app_mention 봉투에는 files 가 없고, 첨부를 가진 message 사본은 위
442
+ // shouldProcessSlackEvent 에서 mention_via_app_mention 으로 드롭된다.
443
+ // 그래서 멘션과 함께 올린 파일은 여기서 되찾지 않으면 영영 사라진다.
444
+ if (!hasFiles && event.type === 'app_mention' && event.channel && event.ts) {
445
+ const recoverToken = getSlackSendClient().token;
446
+ if (recoverToken) {
447
+ const recovered = await recoverSlackAttachments(recoverToken, event.channel, event.ts, event.thread_ts ? { threadTs: event.thread_ts } : {});
448
+ if (recovered.length) {
449
+ event.files = recovered;
450
+ hasFiles = true;
451
+ log.info(`[slack:recover] ${event.channel} ts=${event.ts}: ${recovered.length} attachment(s)`);
452
+ }
289
453
  }
290
454
  }
455
+ if (!text && !hasFiles)
456
+ return;
457
+ if (text)
458
+ log.info(`[slack:in] ${event.channel}: ${redactOutboundText(text).slice(0, 80)}`);
459
+ if (!hasFiles && isResetIntent(text)) {
460
+ const client = getSlackSendClient();
461
+ const result = submitMessage(text, { origin: 'slack', target });
462
+ if (client.token) {
463
+ await sendSlackText(client.token, target, result.action === 'rejected'
464
+ ? t('ws.agentBusy', {}, currentLocale())
465
+ : t('tg.resetDone', {}, currentLocale()));
466
+ }
467
+ return;
468
+ }
469
+ prefetchHandedOff = enqueueSlackIngress(slackIngressLaneKey(target), signal => processSlackMessageEvent(event, target, text, signal, {
470
+ prefetchToken,
471
+ ...(reservedEventKey ? { eventKey: reservedEventKey } : {}),
472
+ ...(reservationGeneration !== undefined ? { reservationGeneration } : {}),
473
+ }));
291
474
  }
292
- if (!text && !hasFiles)
293
- return;
294
- if (text)
295
- log.info(`[slack:in] ${event.channel}: ${redactOutboundText(text).slice(0, 80)}`);
296
- if (!hasFiles && isResetIntent(text)) {
297
- const client = getSlackSendClient();
298
- const result = submitMessage(text, { origin: 'slack', target });
299
- if (client.token) {
300
- await sendSlackText(client.token, target, result.action === 'rejected'
301
- ? t('ws.agentBusy', {}, currentLocale())
302
- : t('tg.resetDone', {}, currentLocale()));
475
+ finally {
476
+ if (prefetchToken && !prefetchHandedOff) {
477
+ releaseThreadPrefetch(event.channel || '', event.thread_ts || '', prefetchToken);
303
478
  }
304
- return;
305
479
  }
306
- enqueueSlackIngress(slackIngressLaneKey(target), signal => processSlackMessageEvent(event, target, text, signal));
307
480
  }
308
481
  // ─── Init / Shutdown ────────────────────────────────
309
482
  export async function initSlack() {
@@ -396,6 +569,12 @@ async function disposeSlackRuntime() {
396
569
  // Identity is cached per (team, id). A re-init can authenticate against a
397
570
  // different workspace, so the cache must not outlive the runtime that filled it.
398
571
  resetSlackIdentityCache();
572
+ // Same reasoning for channel names and thread participants: a workspace
573
+ // switch would otherwise attribute the previous team's conversations.
574
+ resetSlackConversationCache();
575
+ // Prefetch claims are per-runtime: a fresh runtime has no agent session, so
576
+ // the next message in a thread should get its history again.
577
+ resetThreadPrefetchClaims();
399
578
  if (forwarderHandler) {
400
579
  removeBroadcastListener(forwarderHandler);
401
580
  forwarderHandler = null;