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.
- package/README.md +94 -3
- package/dist/server.js +11 -0
- package/dist/server.js.map +1 -1
- package/dist/src/agent/spawn/process-kill.js +96 -2
- package/dist/src/agent/spawn/process-kill.js.map +1 -1
- package/dist/src/agent/spawn.js +34 -65
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/bgtask/runner.js +26 -21
- package/dist/src/bgtask/runner.js.map +1 -1
- package/dist/src/cli/acp-client.js +11 -5
- package/dist/src/cli/acp-client.js.map +1 -1
- package/dist/src/cli/employee-handler.js +3 -2
- package/dist/src/cli/employee-handler.js.map +1 -1
- package/dist/src/code-mode/acp-host.js +22 -10
- package/dist/src/code-mode/acp-host.js.map +1 -1
- package/dist/src/core/config.js +14 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +16 -0
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/employees.js +22 -4
- package/dist/src/core/employees.js.map +1 -1
- package/dist/src/core/windows-shell.js +63 -0
- package/dist/src/core/windows-shell.js.map +1 -0
- package/dist/src/discord/bot.js +199 -147
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/gateway-supervisor.js +303 -0
- package/dist/src/discord/gateway-supervisor.js.map +1 -0
- package/dist/src/discord/rest-scheduler.js +448 -0
- package/dist/src/discord/rest-scheduler.js.map +1 -0
- package/dist/src/discord/send-only-client.js +59 -43
- package/dist/src/discord/send-only-client.js.map +1 -1
- package/dist/src/manager/lifecycle.js +7 -2
- package/dist/src/manager/lifecycle.js.map +1 -1
- package/dist/src/manager/notes/capabilities.js +9 -7
- package/dist/src/manager/notes/capabilities.js.map +1 -1
- package/dist/src/memory/bootstrap.js +5 -1
- package/dist/src/memory/bootstrap.js.map +1 -1
- package/dist/src/memory/host-toolchain.js +156 -0
- package/dist/src/memory/host-toolchain.js.map +1 -0
- package/dist/src/messaging/channel-capabilities.js +30 -0
- package/dist/src/messaging/channel-capabilities.js.map +1 -0
- package/dist/src/messaging/delivery-outcome.js +73 -0
- package/dist/src/messaging/delivery-outcome.js.map +1 -0
- package/dist/src/messaging/draft-stream.js +154 -0
- package/dist/src/messaging/draft-stream.js.map +1 -0
- package/dist/src/messaging/runtime.js +3 -9
- package/dist/src/messaging/runtime.js.map +1 -1
- package/dist/src/messaging/send.js +40 -8
- package/dist/src/messaging/send.js.map +1 -1
- package/dist/src/messaging/types.js +22 -1
- package/dist/src/messaging/types.js.map +1 -1
- package/dist/src/notes/search.js +6 -4
- package/dist/src/notes/search.js.map +1 -1
- package/dist/src/prompt/builder.js +110 -6
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/conversation-context.js +23 -0
- package/dist/src/prompt/conversation-context.js.map +1 -0
- package/dist/src/prompt/templates/a1-system.md +28 -12
- package/dist/src/prompt/templates/control-system.md +19 -6
- package/dist/src/prompt/templates/employee.md +16 -3
- package/dist/src/security/path-guards.js +124 -27
- package/dist/src/security/path-guards.js.map +1 -1
- package/dist/src/slack/api.js +38 -5
- package/dist/src/slack/api.js.map +1 -1
- package/dist/src/slack/bot.js +214 -35
- package/dist/src/slack/bot.js.map +1 -1
- package/dist/src/slack/context.js +192 -0
- package/dist/src/slack/context.js.map +1 -0
- package/dist/src/slack/conversation.js +275 -0
- package/dist/src/slack/conversation.js.map +1 -0
- package/dist/src/slack/enrichment-cache.js +334 -0
- package/dist/src/slack/enrichment-cache.js.map +1 -0
- package/dist/src/slack/history.js +47 -8
- package/dist/src/slack/history.js.map +1 -1
- package/dist/src/slack/identity.js +99 -190
- package/dist/src/slack/identity.js.map +1 -1
- package/dist/src/slack/ingress.js +78 -2
- package/dist/src/slack/ingress.js.map +1 -1
- package/dist/src/slack/progress.js +39 -51
- package/dist/src/slack/progress.js.map +1 -1
- package/dist/src/slack/thread-tracker.js +78 -0
- package/dist/src/slack/thread-tracker.js.map +1 -1
- package/dist/src/telegram/bot.js +105 -56
- package/dist/src/telegram/bot.js.map +1 -1
- package/dist/src/telegram/update-offset.js +152 -0
- package/dist/src/telegram/update-offset.js.map +1 -0
- package/package.json +1 -1
- package/public/dist/assets/Agent-C5EvX-_v.js +1 -0
- package/public/dist/assets/{manager-Bjuh1jpq.js → manager-CP-Au860.js} +1 -1
- package/public/dist/manager/index.html +1 -1
- package/public/manager/src/settings/cli-status-polling.ts +84 -0
- package/public/manager/src/settings/pages/Agent.tsx +84 -5
- package/scripts/install-officecli.ps1 +38 -2
- package/scripts/install.ps1 +1 -1
- 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
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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 =
|
|
115
|
-
const canonJaw =
|
|
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 =
|
|
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
|
-
|
|
126
|
-
|
|
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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"}
|
package/dist/src/slack/api.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: err,
|
|
167
|
+
status: response.status,
|
|
168
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
169
|
+
data: parsed,
|
|
170
|
+
};
|
|
143
171
|
}
|
|
144
|
-
return {
|
|
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":"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;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/slack/api.ts"],"names":[],"mappings":"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"}
|
package/dist/src/slack/bot.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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;
|