dorkos 0.16.0 → 0.17.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.
- package/dist/bin/cli.js +61 -24
- package/dist/client/assets/{TopologyGraph-BdZchvqT.js → TopologyGraph-oKjiYKnE.js} +1 -1
- package/dist/client/assets/{highlighted-body-TPN3WLV5-CMKzPREW.js → highlighted-body-TPN3WLV5-BSyHe17r.js} +1 -1
- package/dist/client/assets/index-1b_G52ou.css +1 -0
- package/dist/client/assets/{index-D938z85i.js → index-KNXnL08A.js} +210 -210
- package/dist/client/index.html +2 -2
- package/dist/drizzle/0006_cooing_moonstone.sql +1 -0
- package/dist/drizzle/meta/0006_snapshot.json +667 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/server/index.js +976 -755
- package/dist/server/index.js.map +4 -4
- package/package.json +3 -3
- package/dist/client/assets/index-DmxrnpKh.css +0 -1
package/dist/server/index.js
CHANGED
|
@@ -71060,7 +71060,7 @@ var SERVER_VERSION = resolveVersion();
|
|
|
71060
71060
|
var IS_DEV_BUILD = checkDevBuild(SERVER_VERSION);
|
|
71061
71061
|
function resolveVersion() {
|
|
71062
71062
|
if (env.DORKOS_VERSION_OVERRIDE) return env.DORKOS_VERSION_OVERRIDE;
|
|
71063
|
-
if (true) return "0.
|
|
71063
|
+
if (true) return "0.17.0";
|
|
71064
71064
|
const pkgPath = path4.join(path4.dirname(fileURLToPath2(import.meta.url)), "../../package.json");
|
|
71065
71065
|
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
71066
71066
|
}
|
|
@@ -72143,7 +72143,8 @@ var ResponseContextSchema = z9.object({
|
|
|
72143
72143
|
platform: z9.string(),
|
|
72144
72144
|
maxLength: z9.number().int().optional(),
|
|
72145
72145
|
supportedFormats: z9.array(z9.string()).optional(),
|
|
72146
|
-
instructions: z9.string().optional()
|
|
72146
|
+
instructions: z9.string().optional(),
|
|
72147
|
+
formattingInstructions: z9.string().optional()
|
|
72147
72148
|
}).openapi("ResponseContext");
|
|
72148
72149
|
var StandardPayloadSchema = z9.object({
|
|
72149
72150
|
content: z9.string(),
|
|
@@ -74473,6 +74474,20 @@ var TranscriptReader = class {
|
|
|
74473
74474
|
const slug = this.getProjectSlug(vaultRoot2);
|
|
74474
74475
|
return path14.join(os3.homedir(), ".claude", "projects", slug);
|
|
74475
74476
|
}
|
|
74477
|
+
/**
|
|
74478
|
+
* Check whether a JSONL transcript file exists for the given session ID.
|
|
74479
|
+
* Lightweight stat-only check (no parsing). Skips boundary validation
|
|
74480
|
+
* since the caller is expected to have already validated.
|
|
74481
|
+
*/
|
|
74482
|
+
async hasTranscript(vaultRoot2, sessionId) {
|
|
74483
|
+
const filePath = path14.join(this.getTranscriptsDir(vaultRoot2), `${sessionId}.jsonl`);
|
|
74484
|
+
try {
|
|
74485
|
+
await fs8.access(filePath);
|
|
74486
|
+
return true;
|
|
74487
|
+
} catch {
|
|
74488
|
+
return false;
|
|
74489
|
+
}
|
|
74490
|
+
}
|
|
74476
74491
|
/**
|
|
74477
74492
|
* List all sessions by scanning SDK JSONL transcript files.
|
|
74478
74493
|
* Extracts metadata (title, timestamps, preview) from file content and stats.
|
|
@@ -75474,9 +75489,9 @@ var NodeFsHandler = class {
|
|
|
75474
75489
|
if (this.fsw.closed) {
|
|
75475
75490
|
return;
|
|
75476
75491
|
}
|
|
75477
|
-
const
|
|
75492
|
+
const dirname8 = sysPath.dirname(file);
|
|
75478
75493
|
const basename4 = sysPath.basename(file);
|
|
75479
|
-
const parent = this.fsw._getWatchedDir(
|
|
75494
|
+
const parent = this.fsw._getWatchedDir(dirname8);
|
|
75480
75495
|
let prevStats = stats;
|
|
75481
75496
|
if (parent.has(basename4))
|
|
75482
75497
|
return;
|
|
@@ -75503,7 +75518,7 @@ var NodeFsHandler = class {
|
|
|
75503
75518
|
prevStats = newStats2;
|
|
75504
75519
|
}
|
|
75505
75520
|
} catch (error) {
|
|
75506
|
-
this.fsw._remove(
|
|
75521
|
+
this.fsw._remove(dirname8, basename4);
|
|
75507
75522
|
}
|
|
75508
75523
|
} else if (parent.has(basename4)) {
|
|
75509
75524
|
const at = newStats.atimeMs;
|
|
@@ -77884,6 +77899,11 @@ ${messageOpts.systemPromptAppend}` : baseAppend;
|
|
|
77884
77899
|
};
|
|
77885
77900
|
if (session.hasStarted) {
|
|
77886
77901
|
sdkOptions.resume = session.sdkSessionId;
|
|
77902
|
+
if (session.sdkSessionId === sessionId) {
|
|
77903
|
+
logger.debug("[sendMessage] resuming with sdkSessionId === sessionId (expected after server restart)", {
|
|
77904
|
+
session: sessionId
|
|
77905
|
+
});
|
|
77906
|
+
}
|
|
77887
77907
|
}
|
|
77888
77908
|
const cwdSource = messageOpts?.cwd ? "opts.cwd" : opts.sessionCwd ? "session.cwd" : "default";
|
|
77889
77909
|
logger.debug("[sendMessage]", {
|
|
@@ -78214,8 +78234,9 @@ var ClaudeCodeRuntime = class _ClaudeCodeRuntime {
|
|
|
78214
78234
|
if (!session) {
|
|
78215
78235
|
this.ensureSession(sessionId, {
|
|
78216
78236
|
permissionMode: opts.permissionMode ?? "default",
|
|
78217
|
-
hasStarted:
|
|
78237
|
+
hasStarted: false
|
|
78218
78238
|
});
|
|
78239
|
+
this.sessions.get(sessionId).needsTranscriptCheck = true;
|
|
78219
78240
|
session = this.sessions.get(sessionId);
|
|
78220
78241
|
}
|
|
78221
78242
|
if (opts.permissionMode) {
|
|
@@ -78241,11 +78262,31 @@ var ClaudeCodeRuntime = class _ClaudeCodeRuntime {
|
|
|
78241
78262
|
// ---------------------------------------------------------------------------
|
|
78242
78263
|
async *sendMessage(sessionId, content3, opts) {
|
|
78243
78264
|
if (!this.sessions.has(sessionId)) {
|
|
78265
|
+
const effectiveCwd = opts?.cwd ?? this.cwd;
|
|
78266
|
+
const hasTranscript = await this.transcriptReader.hasTranscript(effectiveCwd, sessionId);
|
|
78267
|
+
logger.debug("[sendMessage] auto-creating session", {
|
|
78268
|
+
session: sessionId,
|
|
78269
|
+
hasTranscript,
|
|
78270
|
+
cwd: effectiveCwd
|
|
78271
|
+
});
|
|
78244
78272
|
this.ensureSession(sessionId, {
|
|
78245
78273
|
permissionMode: opts?.permissionMode ?? "default",
|
|
78246
78274
|
cwd: opts?.cwd,
|
|
78247
|
-
hasStarted:
|
|
78275
|
+
hasStarted: hasTranscript
|
|
78248
78276
|
});
|
|
78277
|
+
} else {
|
|
78278
|
+
const existingSession = this.sessions.get(sessionId);
|
|
78279
|
+
if (existingSession.needsTranscriptCheck) {
|
|
78280
|
+
existingSession.needsTranscriptCheck = false;
|
|
78281
|
+
const effectiveCwd = opts?.cwd || existingSession.cwd || this.cwd;
|
|
78282
|
+
const hasTranscript = await this.transcriptReader.hasTranscript(effectiveCwd, sessionId);
|
|
78283
|
+
if (hasTranscript) {
|
|
78284
|
+
logger.debug("[sendMessage] upgrading hasStarted for existing transcript", {
|
|
78285
|
+
session: sessionId
|
|
78286
|
+
});
|
|
78287
|
+
existingSession.hasStarted = true;
|
|
78288
|
+
}
|
|
78289
|
+
}
|
|
78249
78290
|
}
|
|
78250
78291
|
const session = this.sessions.get(sessionId);
|
|
78251
78292
|
yield* executeSdkQuery(sessionId, content3, session, {
|
|
@@ -84670,6 +84711,7 @@ var relayIndex = sqliteTable("relay_index", {
|
|
|
84670
84711
|
}).notNull().default("pending"),
|
|
84671
84712
|
expiresAt: text("expires_at"),
|
|
84672
84713
|
// was: ttl INTEGER (Unix ms)
|
|
84714
|
+
sender: text("sender"),
|
|
84673
84715
|
payload: text("payload"),
|
|
84674
84716
|
metadata: text("metadata"),
|
|
84675
84717
|
createdAt: text("created_at").notNull()
|
|
@@ -86876,7 +86918,8 @@ var SqliteIndex = class {
|
|
|
86876
86918
|
endpointHash: message.endpointHash,
|
|
86877
86919
|
status: message.status,
|
|
86878
86920
|
createdAt: message.createdAt,
|
|
86879
|
-
expiresAt: message.expiresAt
|
|
86921
|
+
expiresAt: message.expiresAt,
|
|
86922
|
+
sender: message.sender ?? null
|
|
86880
86923
|
}).onConflictDoUpdate({
|
|
86881
86924
|
target: relayIndex.id,
|
|
86882
86925
|
set: {
|
|
@@ -86884,7 +86927,8 @@ var SqliteIndex = class {
|
|
|
86884
86927
|
endpointHash: message.endpointHash,
|
|
86885
86928
|
status: message.status,
|
|
86886
86929
|
createdAt: message.createdAt,
|
|
86887
|
-
expiresAt: message.expiresAt
|
|
86930
|
+
expiresAt: message.expiresAt,
|
|
86931
|
+
sender: message.sender ?? null
|
|
86888
86932
|
}
|
|
86889
86933
|
}).run();
|
|
86890
86934
|
}
|
|
@@ -86931,16 +86975,15 @@ var SqliteIndex = class {
|
|
|
86931
86975
|
return rows.map(mapRow);
|
|
86932
86976
|
}
|
|
86933
86977
|
/**
|
|
86934
|
-
* Count messages
|
|
86935
|
-
* Used by the rate limiter for sliding window log checks.
|
|
86978
|
+
* Count messages from a specific sender within a time window.
|
|
86979
|
+
* Used by the rate limiter for per-sender sliding window log checks.
|
|
86936
86980
|
*
|
|
86937
|
-
* @param sender -
|
|
86938
|
-
* is now done at the RelayCore level before indexing.
|
|
86981
|
+
* @param sender - The sender identity to filter by (e.g. `relay.human.slack.bot`).
|
|
86939
86982
|
* @param windowStartIso - ISO 8601 timestamp marking the start of the window.
|
|
86940
|
-
* @returns The number of messages after the window start.
|
|
86983
|
+
* @returns The number of messages from this sender after the window start.
|
|
86941
86984
|
*/
|
|
86942
|
-
countSenderInWindow(
|
|
86943
|
-
const rows = this.db.select({ cnt: count() }).from(relayIndex).where(sql`${relayIndex.createdAt} > ${windowStartIso}
|
|
86985
|
+
countSenderInWindow(sender, windowStartIso) {
|
|
86986
|
+
const rows = this.db.select({ cnt: count() }).from(relayIndex).where(and(sql`${relayIndex.createdAt} > ${windowStartIso}`, eq(relayIndex.sender, sender))).all();
|
|
86944
86987
|
return rows[0]?.cnt ?? 0;
|
|
86945
86988
|
}
|
|
86946
86989
|
/**
|
|
@@ -86958,7 +87001,7 @@ var SqliteIndex = class {
|
|
|
86958
87001
|
/**
|
|
86959
87002
|
* Query messages with optional filters and cursor-based pagination.
|
|
86960
87003
|
*
|
|
86961
|
-
* Supports filtering by subject, status, sender
|
|
87004
|
+
* Supports filtering by subject, status, sender, and endpoint hash.
|
|
86962
87005
|
* Uses ULID cursor for pagination (messages are sorted by id DESC).
|
|
86963
87006
|
*
|
|
86964
87007
|
* @param filters - Optional query filters
|
|
@@ -86972,6 +87015,9 @@ var SqliteIndex = class {
|
|
|
86972
87015
|
if (filters?.status) {
|
|
86973
87016
|
conditions.push(eq(relayIndex.status, filters.status));
|
|
86974
87017
|
}
|
|
87018
|
+
if (filters?.sender) {
|
|
87019
|
+
conditions.push(eq(relayIndex.sender, filters.sender));
|
|
87020
|
+
}
|
|
86975
87021
|
if (filters?.endpointHash) {
|
|
86976
87022
|
conditions.push(eq(relayIndex.endpointHash, filters.endpointHash));
|
|
86977
87023
|
}
|
|
@@ -87042,7 +87088,8 @@ var SqliteIndex = class {
|
|
|
87042
87088
|
endpointHash: hash,
|
|
87043
87089
|
status: statusMap[subdir],
|
|
87044
87090
|
createdAt: envelope.createdAt,
|
|
87045
|
-
expiresAt: envelope.budget.ttl ? new Date(envelope.budget.ttl).toISOString() : null
|
|
87091
|
+
expiresAt: envelope.budget.ttl ? new Date(envelope.budget.ttl).toISOString() : null,
|
|
87092
|
+
sender: envelope.from
|
|
87046
87093
|
});
|
|
87047
87094
|
rebuildCount++;
|
|
87048
87095
|
}
|
|
@@ -87110,7 +87157,8 @@ function mapRow(row) {
|
|
|
87110
87157
|
endpointHash: row.endpointHash,
|
|
87111
87158
|
status: row.status,
|
|
87112
87159
|
createdAt: row.createdAt,
|
|
87113
|
-
expiresAt: row.expiresAt
|
|
87160
|
+
expiresAt: row.expiresAt,
|
|
87161
|
+
sender: row.sender
|
|
87114
87162
|
};
|
|
87115
87163
|
}
|
|
87116
87164
|
async function listMessageIds(store, hash, subdir) {
|
|
@@ -88239,6 +88287,7 @@ var RelayPublishPipeline = class {
|
|
|
88239
88287
|
const countInWindow = this.deps.sqliteIndex.countSenderInWindow(options.from, windowStartIso);
|
|
88240
88288
|
const rateLimitResult = checkRateLimit(options.from, countInWindow, this.rateLimitConfig);
|
|
88241
88289
|
if (!rateLimitResult.allowed) {
|
|
88290
|
+
this.deps.logger?.warn?.(`publish rate-limited: sender=${options.from}, count=${rateLimitResult.currentCount}/${rateLimitResult.limit} in ${this.rateLimitConfig.windowSecs}s window, subject=${subject}`);
|
|
88242
88291
|
return {
|
|
88243
88292
|
messageId: "",
|
|
88244
88293
|
deliveredTo: 0,
|
|
@@ -88262,6 +88311,16 @@ var RelayPublishPipeline = class {
|
|
|
88262
88311
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88263
88312
|
payload
|
|
88264
88313
|
};
|
|
88314
|
+
this.deps.sqliteIndex.insertMessage({
|
|
88315
|
+
id: messageId,
|
|
88316
|
+
subject,
|
|
88317
|
+
endpointHash: "*",
|
|
88318
|
+
// placeholder — not a Maildir endpoint
|
|
88319
|
+
status: "delivered",
|
|
88320
|
+
createdAt: envelope.createdAt,
|
|
88321
|
+
expiresAt: envelope.budget.ttl ? new Date(envelope.budget.ttl).toISOString() : null,
|
|
88322
|
+
sender: options.from
|
|
88323
|
+
});
|
|
88265
88324
|
return this.deliverAndFinalize(envelope, subject, options, messageId);
|
|
88266
88325
|
}
|
|
88267
88326
|
/**
|
|
@@ -88505,7 +88564,8 @@ var RelayCore = class {
|
|
|
88505
88564
|
deliveryPipeline: this.deliveryPipeline,
|
|
88506
88565
|
adapterDelivery,
|
|
88507
88566
|
adapterRegistry: options?.adapterRegistry,
|
|
88508
|
-
traceStore: options?.traceStore
|
|
88567
|
+
traceStore: options?.traceStore,
|
|
88568
|
+
logger: options?.logger
|
|
88509
88569
|
}, {
|
|
88510
88570
|
maxHops: options?.maxHops ?? DEFAULT_MAX_HOPS2,
|
|
88511
88571
|
defaultTtlMs: options?.defaultTtlMs ?? DEFAULT_TTL_MS2,
|
|
@@ -88877,6 +88937,32 @@ var BaseRelayAdapter = class {
|
|
|
88877
88937
|
return;
|
|
88878
88938
|
this._status = { ...this._status, state: "connected" };
|
|
88879
88939
|
}
|
|
88940
|
+
/**
|
|
88941
|
+
* Build a callbacks object for inbound message handling sub-modules.
|
|
88942
|
+
*
|
|
88943
|
+
* Returns bound wrappers around `trackInbound()` and `recordError()` so
|
|
88944
|
+
* sub-modules can update adapter status without holding a reference to the
|
|
88945
|
+
* adapter itself.
|
|
88946
|
+
*/
|
|
88947
|
+
makeInboundCallbacks() {
|
|
88948
|
+
return {
|
|
88949
|
+
trackInbound: () => this.trackInbound(),
|
|
88950
|
+
recordError: (err) => this.recordError(err)
|
|
88951
|
+
};
|
|
88952
|
+
}
|
|
88953
|
+
/**
|
|
88954
|
+
* Build a callbacks object for outbound message delivery sub-modules.
|
|
88955
|
+
*
|
|
88956
|
+
* Returns bound wrappers around `trackOutbound()` and `recordError()` so
|
|
88957
|
+
* sub-modules can update adapter status without holding a reference to the
|
|
88958
|
+
* adapter itself.
|
|
88959
|
+
*/
|
|
88960
|
+
makeOutboundCallbacks() {
|
|
88961
|
+
return {
|
|
88962
|
+
trackOutbound: () => this.trackOutbound(),
|
|
88963
|
+
recordError: (err) => this.recordError(err)
|
|
88964
|
+
};
|
|
88965
|
+
}
|
|
88880
88966
|
/** Whether the adapter has been stopped or is stopping. */
|
|
88881
88967
|
get isStopped() {
|
|
88882
88968
|
return this._status.state === "disconnected" || this._status.state === "stopping";
|
|
@@ -89016,6 +89102,14 @@ var GROUP_SEGMENT = "group";
|
|
|
89016
89102
|
var MAX_MESSAGE_LENGTH = 4096;
|
|
89017
89103
|
var UNKNOWN_SENDER = "unknown";
|
|
89018
89104
|
var MAX_CONTENT_LENGTH = 32768;
|
|
89105
|
+
var TELEGRAM_FORMATTING_RULES = [
|
|
89106
|
+
"FORMATTING RULES (you MUST follow these):",
|
|
89107
|
+
"- Do NOT use Markdown tables. Telegram cannot render them.",
|
|
89108
|
+
"- For structured data: use bullet points or bold key-value pairs.",
|
|
89109
|
+
"- Use **bold**, _italic_, `code`, ```code blocks```, and [links](url).",
|
|
89110
|
+
"- Telegram supports HTML subset: headings are not supported, use bold instead.",
|
|
89111
|
+
`- Keep responses concise. Messages over ${MAX_MESSAGE_LENGTH} characters are split.`
|
|
89112
|
+
].join("\n");
|
|
89019
89113
|
function buildSubject(chatId, isGroup) {
|
|
89020
89114
|
if (isGroup) {
|
|
89021
89115
|
return `${SUBJECT_PREFIX}.${GROUP_SEGMENT}.${chatId}`;
|
|
@@ -89077,7 +89171,8 @@ async function handleInboundMessage(ctx, relay, callbacks, logger3 = noopLogger)
|
|
|
89077
89171
|
platform: "telegram",
|
|
89078
89172
|
maxLength: MAX_MESSAGE_LENGTH,
|
|
89079
89173
|
supportedFormats: ["text", "markdown"],
|
|
89080
|
-
instructions: `Reply to subject ${subject} to respond to this Telegram message
|
|
89174
|
+
instructions: `Reply to subject ${subject} to respond to this Telegram message.`,
|
|
89175
|
+
formattingInstructions: TELEGRAM_FORMATTING_RULES
|
|
89081
89176
|
},
|
|
89082
89177
|
platformData: {
|
|
89083
89178
|
chatId: chat.id,
|
|
@@ -89088,10 +89183,16 @@ async function handleInboundMessage(ctx, relay, callbacks, logger3 = noopLogger)
|
|
|
89088
89183
|
}
|
|
89089
89184
|
};
|
|
89090
89185
|
try {
|
|
89091
|
-
await relay.publish(subject, payload, {
|
|
89186
|
+
const result2 = await relay.publish(subject, payload, {
|
|
89092
89187
|
from: `${SUBJECT_PREFIX}.bot`,
|
|
89093
89188
|
replyTo: subject
|
|
89094
89189
|
});
|
|
89190
|
+
if (result2.deliveredTo === 0 && result2.rejected?.length) {
|
|
89191
|
+
const reason = result2.rejected[0]?.reason ?? "unknown";
|
|
89192
|
+
callbacks.recordError(new Error(`Publish rejected: ${reason}`));
|
|
89193
|
+
logger3.warn(`inbound publish rejected for chat ${chat.id}: ${reason}`);
|
|
89194
|
+
return;
|
|
89195
|
+
}
|
|
89095
89196
|
callbacks.trackInbound();
|
|
89096
89197
|
logger3.debug(`inbound from ${senderName} in chat ${chat.id}: "${text6.slice(0, 80)}${text6.length > 80 ? "\u2026" : ""}" (${text6.length} chars) \u2192 ${subject}`);
|
|
89097
89198
|
} catch (err) {
|
|
@@ -89502,9 +89603,9 @@ var VFile = class {
|
|
|
89502
89603
|
* @returns {undefined}
|
|
89503
89604
|
* Nothing.
|
|
89504
89605
|
*/
|
|
89505
|
-
set dirname(
|
|
89606
|
+
set dirname(dirname8) {
|
|
89506
89607
|
assertPath(this.basename, "dirname");
|
|
89507
|
-
this.path = default2.join(
|
|
89608
|
+
this.path = default2.join(dirname8 || "", this.basename);
|
|
89508
89609
|
}
|
|
89509
89610
|
/**
|
|
89510
89611
|
* Get the extname (including dot) (example: `'.js'`).
|
|
@@ -101527,37 +101628,82 @@ function formatToolDescription(toolName, input) {
|
|
|
101527
101628
|
}
|
|
101528
101629
|
return `wants to use tool \`${toolName}\``;
|
|
101529
101630
|
}
|
|
101631
|
+
function extractAgentIdFromEnvelope(envelope) {
|
|
101632
|
+
const payload = envelope.payload;
|
|
101633
|
+
if (payload && typeof payload === "object" && "data" in payload) {
|
|
101634
|
+
const data = payload.data;
|
|
101635
|
+
if (data && typeof data === "object" && "agentId" in data) {
|
|
101636
|
+
return data.agentId;
|
|
101637
|
+
}
|
|
101638
|
+
}
|
|
101639
|
+
return void 0;
|
|
101640
|
+
}
|
|
101641
|
+
function extractSessionIdFromEnvelope(envelope) {
|
|
101642
|
+
const payload = envelope.payload;
|
|
101643
|
+
if (payload && typeof payload === "object" && "data" in payload) {
|
|
101644
|
+
const data = payload.data;
|
|
101645
|
+
if (data && typeof data === "object" && "ccaSessionKey" in data) {
|
|
101646
|
+
return data.ccaSessionKey;
|
|
101647
|
+
}
|
|
101648
|
+
}
|
|
101649
|
+
return void 0;
|
|
101650
|
+
}
|
|
101651
|
+
function markdownToTelegramHtml(md) {
|
|
101652
|
+
let html2 = md;
|
|
101653
|
+
html2 = html2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
101654
|
+
html2 = html2.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code3) => {
|
|
101655
|
+
const cls = lang ? ` class="language-${lang}"` : "";
|
|
101656
|
+
return `<pre><code${cls}>${code3.trimEnd()}</code></pre>`;
|
|
101657
|
+
});
|
|
101658
|
+
html2 = html2.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
101659
|
+
html2 = html2.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
|
101660
|
+
html2 = html2.replace(/\*(.+?)\*/g, "<i>$1</i>");
|
|
101661
|
+
html2 = html2.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
101662
|
+
html2 = html2.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
|
|
101663
|
+
html2 = html2.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
|
|
101664
|
+
return html2;
|
|
101665
|
+
}
|
|
101530
101666
|
function formatForPlatform(content3, platform2) {
|
|
101531
101667
|
switch (platform2) {
|
|
101532
101668
|
case "slack":
|
|
101533
101669
|
return slackifyMarkdown(content3);
|
|
101534
101670
|
case "telegram":
|
|
101535
|
-
return content3;
|
|
101671
|
+
return markdownToTelegramHtml(content3);
|
|
101536
101672
|
case "plain":
|
|
101537
101673
|
return content3.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`{3}[\s\S]*?`{3}/g, (m3) => m3.replace(/`{3}\w*\n?/g, "").trim()).replace(/`(.+?)`/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^[*-]\s+/gm, "- ").replace(/^>\s+/gm, "").replace(/\[(.+?)\]\(.+?\)/g, "$1");
|
|
101538
101674
|
}
|
|
101539
101675
|
}
|
|
101540
101676
|
|
|
101677
|
+
// ../relay/dist/adapters/telegram/stream-api.js
|
|
101678
|
+
async function sendMessageDraft(bot, chatId, text6) {
|
|
101679
|
+
await bot.api.sendMessageDraft(chatId, text6);
|
|
101680
|
+
}
|
|
101681
|
+
|
|
101541
101682
|
// ../relay/dist/adapters/telegram/outbound.js
|
|
101542
101683
|
var TELEGRAM_TYPING_ACTION = "typing";
|
|
101543
|
-
var typingIntervals = /* @__PURE__ */ new Map();
|
|
101544
101684
|
var TYPING_REFRESH_MS = 4e3;
|
|
101545
101685
|
var DRAFT_UPDATE_INTERVAL_MS = 200;
|
|
101546
101686
|
var BUFFER_TTL_MS = 5 * 60 * 1e3;
|
|
101547
|
-
var lastDraftUpdate = /* @__PURE__ */ new Map();
|
|
101548
|
-
var callbackIdMap = /* @__PURE__ */ new Map();
|
|
101549
101687
|
var CALLBACK_ID_TTL_MS = 15 * 60 * 1e3;
|
|
101550
|
-
|
|
101551
|
-
|
|
101552
|
-
|
|
101688
|
+
function createTelegramOutboundState() {
|
|
101689
|
+
return {
|
|
101690
|
+
typingIntervals: /* @__PURE__ */ new Map(),
|
|
101691
|
+
lastDraftUpdate: /* @__PURE__ */ new Map(),
|
|
101692
|
+
callbackIdMap: /* @__PURE__ */ new Map(),
|
|
101693
|
+
pendingApprovalTimeouts: /* @__PURE__ */ new Map()
|
|
101694
|
+
};
|
|
101695
|
+
}
|
|
101696
|
+
function clearApprovalTimeout(state, shortKey) {
|
|
101697
|
+
const timer = state.pendingApprovalTimeouts.get(shortKey);
|
|
101553
101698
|
if (timer) {
|
|
101554
101699
|
clearTimeout(timer);
|
|
101555
|
-
pendingApprovalTimeouts.delete(shortKey);
|
|
101700
|
+
state.pendingApprovalTimeouts.delete(shortKey);
|
|
101556
101701
|
}
|
|
101557
101702
|
}
|
|
101558
101703
|
async function sendAndTrack(bot, chatId, text6, startTime, callbacks) {
|
|
101559
101704
|
try {
|
|
101560
|
-
|
|
101705
|
+
const html2 = formatForPlatform(text6, "telegram");
|
|
101706
|
+
await bot.api.sendMessage(chatId, html2, { parse_mode: "HTML" });
|
|
101561
101707
|
callbacks.trackOutbound();
|
|
101562
101708
|
return { success: true, durationMs: Date.now() - startTime };
|
|
101563
101709
|
} catch (err) {
|
|
@@ -101570,7 +101716,7 @@ async function sendAndTrack(bot, chatId, text6, startTime, callbacks) {
|
|
|
101570
101716
|
}
|
|
101571
101717
|
}
|
|
101572
101718
|
async function deliverMessage(opts) {
|
|
101573
|
-
const { adapterId, subject, envelope, bot, responseBuffers, callbacks, streaming, logger: logger3 = noopLogger } = opts;
|
|
101719
|
+
const { adapterId, subject, envelope, bot, responseBuffers, state, callbacks, streaming, logger: logger3 = noopLogger } = opts;
|
|
101574
101720
|
const startTime = Date.now();
|
|
101575
101721
|
if (envelope.from.startsWith(SUBJECT_PREFIX)) {
|
|
101576
101722
|
logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
|
|
@@ -101595,7 +101741,7 @@ async function deliverMessage(opts) {
|
|
|
101595
101741
|
for (const [id, buf] of responseBuffers) {
|
|
101596
101742
|
if (now - buf.startedAt > BUFFER_TTL_MS) {
|
|
101597
101743
|
responseBuffers.delete(id);
|
|
101598
|
-
lastDraftUpdate.delete(id);
|
|
101744
|
+
state.lastDraftUpdate.delete(id);
|
|
101599
101745
|
logger3.warn(`buffer: reaped stale buffer for chat ${id} (age: ${Math.round((now - buf.startedAt) / 1e3)}s)`);
|
|
101600
101746
|
}
|
|
101601
101747
|
}
|
|
@@ -101610,12 +101756,12 @@ async function deliverMessage(opts) {
|
|
|
101610
101756
|
startedAt: existing?.startedAt ?? Date.now()
|
|
101611
101757
|
});
|
|
101612
101758
|
if (streaming && chatId > 0) {
|
|
101613
|
-
const lastUpdate = lastDraftUpdate.get(chatId) ?? 0;
|
|
101759
|
+
const lastUpdate = state.lastDraftUpdate.get(chatId) ?? 0;
|
|
101614
101760
|
if (Date.now() - lastUpdate >= DRAFT_UPDATE_INTERVAL_MS) {
|
|
101615
|
-
lastDraftUpdate.set(chatId, Date.now());
|
|
101761
|
+
state.lastDraftUpdate.set(chatId, Date.now());
|
|
101616
101762
|
logger3.debug(`stream: sendMessageDraft to chat ${chatId} (${responseBuffers.get(chatId).text.length} chars)`);
|
|
101617
101763
|
try {
|
|
101618
|
-
await
|
|
101764
|
+
await sendMessageDraft(bot, chatId, responseBuffers.get(chatId).text);
|
|
101619
101765
|
} catch {
|
|
101620
101766
|
}
|
|
101621
101767
|
}
|
|
@@ -101627,7 +101773,7 @@ async function deliverMessage(opts) {
|
|
|
101627
101773
|
logger3.debug(`deliver: error to chat ${chatId}: "${errorMsg.slice(0, 100)}"`);
|
|
101628
101774
|
const buffered = responseBuffers.get(chatId)?.text ?? "";
|
|
101629
101775
|
responseBuffers.delete(chatId);
|
|
101630
|
-
lastDraftUpdate.delete(chatId);
|
|
101776
|
+
state.lastDraftUpdate.delete(chatId);
|
|
101631
101777
|
const text7 = buffered ? truncateText(`${buffered}
|
|
101632
101778
|
|
|
101633
101779
|
[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH) : truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH);
|
|
@@ -101637,7 +101783,7 @@ async function deliverMessage(opts) {
|
|
|
101637
101783
|
const buffered = responseBuffers.get(chatId);
|
|
101638
101784
|
logger3.debug(`deliver: done for chat ${chatId} (buffered: ${buffered ? `${buffered.text.length} chars` : "empty"})`);
|
|
101639
101785
|
responseBuffers.delete(chatId);
|
|
101640
|
-
lastDraftUpdate.delete(chatId);
|
|
101786
|
+
state.lastDraftUpdate.delete(chatId);
|
|
101641
101787
|
if (buffered) {
|
|
101642
101788
|
return sendAndTrack(bot, chatId, truncateText(buffered.text, MAX_MESSAGE_LENGTH), startTime, callbacks);
|
|
101643
101789
|
}
|
|
@@ -101650,10 +101796,10 @@ async function deliverMessage(opts) {
|
|
|
101650
101796
|
const buffered = responseBuffers.get(chatId);
|
|
101651
101797
|
if (buffered?.text) {
|
|
101652
101798
|
responseBuffers.delete(chatId);
|
|
101653
|
-
lastDraftUpdate.delete(chatId);
|
|
101799
|
+
state.lastDraftUpdate.delete(chatId);
|
|
101654
101800
|
await sendAndTrack(bot, chatId, truncateText(buffered.text, MAX_MESSAGE_LENGTH), startTime, callbacks);
|
|
101655
101801
|
}
|
|
101656
|
-
return handleApprovalRequired(bot, chatId, data, envelope, callbacks, startTime);
|
|
101802
|
+
return handleApprovalRequired(bot, chatId, data, envelope, state, callbacks, startTime);
|
|
101657
101803
|
}
|
|
101658
101804
|
}
|
|
101659
101805
|
logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
|
|
@@ -101664,14 +101810,14 @@ async function deliverMessage(opts) {
|
|
|
101664
101810
|
logger3.debug(`deliver: standard payload to chat ${chatId} (${text6.length} chars)`);
|
|
101665
101811
|
return sendAndTrack(bot, chatId, text6, startTime, callbacks);
|
|
101666
101812
|
}
|
|
101667
|
-
async function handleTypingSignal(bot, subject,
|
|
101813
|
+
async function handleTypingSignal(bot, subject, outboundState, signalState) {
|
|
101668
101814
|
if (!bot)
|
|
101669
101815
|
return;
|
|
101670
101816
|
const chatId = extractChatId(subject);
|
|
101671
101817
|
if (chatId === null)
|
|
101672
101818
|
return;
|
|
101673
|
-
if (
|
|
101674
|
-
clearTypingInterval(chatId);
|
|
101819
|
+
if (signalState === "active") {
|
|
101820
|
+
clearTypingInterval(outboundState, chatId);
|
|
101675
101821
|
try {
|
|
101676
101822
|
await bot.api.sendChatAction(chatId, TELEGRAM_TYPING_ACTION);
|
|
101677
101823
|
} catch {
|
|
@@ -101680,43 +101826,33 @@ async function handleTypingSignal(bot, subject, state) {
|
|
|
101680
101826
|
try {
|
|
101681
101827
|
await bot.api.sendChatAction(chatId, TELEGRAM_TYPING_ACTION);
|
|
101682
101828
|
} catch {
|
|
101683
|
-
clearTypingInterval(chatId);
|
|
101829
|
+
clearTypingInterval(outboundState, chatId);
|
|
101684
101830
|
}
|
|
101685
101831
|
}, TYPING_REFRESH_MS);
|
|
101686
|
-
typingIntervals.set(chatId, intervalId);
|
|
101832
|
+
outboundState.typingIntervals.set(chatId, intervalId);
|
|
101687
101833
|
} else {
|
|
101688
|
-
clearTypingInterval(chatId);
|
|
101834
|
+
clearTypingInterval(outboundState, chatId);
|
|
101689
101835
|
}
|
|
101690
101836
|
}
|
|
101691
|
-
function clearTypingInterval(chatId) {
|
|
101692
|
-
const existing = typingIntervals.get(chatId);
|
|
101837
|
+
function clearTypingInterval(state, chatId) {
|
|
101838
|
+
const existing = state.typingIntervals.get(chatId);
|
|
101693
101839
|
if (existing !== void 0) {
|
|
101694
101840
|
clearInterval(existing);
|
|
101695
|
-
typingIntervals.delete(chatId);
|
|
101841
|
+
state.typingIntervals.delete(chatId);
|
|
101696
101842
|
}
|
|
101697
101843
|
}
|
|
101698
|
-
function clearAllTypingIntervals() {
|
|
101699
|
-
for (const interval of typingIntervals.values())
|
|
101844
|
+
function clearAllTypingIntervals(state) {
|
|
101845
|
+
for (const interval of state.typingIntervals.values())
|
|
101700
101846
|
clearInterval(interval);
|
|
101701
|
-
typingIntervals.clear();
|
|
101702
|
-
lastDraftUpdate.clear();
|
|
101847
|
+
state.typingIntervals.clear();
|
|
101848
|
+
state.lastDraftUpdate.clear();
|
|
101703
101849
|
}
|
|
101704
|
-
function
|
|
101705
|
-
const
|
|
101706
|
-
const
|
|
101707
|
-
return data?.agentId ?? "unknown";
|
|
101708
|
-
}
|
|
101709
|
-
function extractSessionIdFromEnvelope(envelope) {
|
|
101710
|
-
const payload = envelope.payload;
|
|
101711
|
-
const data = payload?.data;
|
|
101712
|
-
return data?.ccaSessionKey ?? "unknown";
|
|
101713
|
-
}
|
|
101714
|
-
async function handleApprovalRequired(bot, chatId, data, envelope, callbacks, startTime) {
|
|
101715
|
-
const agentId = extractAgentIdFromEnvelope(envelope);
|
|
101716
|
-
const sessionId = extractSessionIdFromEnvelope(envelope);
|
|
101850
|
+
async function handleApprovalRequired(bot, chatId, data, envelope, state, callbacks, startTime) {
|
|
101851
|
+
const agentId = extractAgentIdFromEnvelope(envelope) ?? "unknown";
|
|
101852
|
+
const sessionId = extractSessionIdFromEnvelope(envelope) ?? "unknown";
|
|
101717
101853
|
const shortKey = randomBytes(6).toString("hex");
|
|
101718
|
-
callbackIdMap.set(shortKey, { toolCallId: data.toolCallId, sessionId, agentId });
|
|
101719
|
-
setTimeout(() => callbackIdMap.delete(shortKey), CALLBACK_ID_TTL_MS);
|
|
101854
|
+
state.callbackIdMap.set(shortKey, { toolCallId: data.toolCallId, sessionId, agentId });
|
|
101855
|
+
setTimeout(() => state.callbackIdMap.delete(shortKey), CALLBACK_ID_TTL_MS);
|
|
101720
101856
|
const toolDescription = formatToolDescription(data.toolName, data.input);
|
|
101721
101857
|
const inputPreview = truncateText(data.input, 400);
|
|
101722
101858
|
const messageText = `*Tool Approval Required*
|
|
@@ -101739,15 +101875,15 @@ ${inputPreview}
|
|
|
101739
101875
|
});
|
|
101740
101876
|
if (data.timeoutMs && data.timeoutMs > 0) {
|
|
101741
101877
|
const timer = setTimeout(async () => {
|
|
101742
|
-
pendingApprovalTimeouts.delete(shortKey);
|
|
101743
|
-
callbackIdMap.delete(shortKey);
|
|
101878
|
+
state.pendingApprovalTimeouts.delete(shortKey);
|
|
101879
|
+
state.callbackIdMap.delete(shortKey);
|
|
101744
101880
|
try {
|
|
101745
101881
|
await bot.api.editMessageText(chatId, sent.message_id, `\u23F0 *Tool Approval Timed Out*
|
|
101746
101882
|
~~\`${data.toolName}\`~~ ${toolDescription}`, { parse_mode: "Markdown" });
|
|
101747
101883
|
} catch {
|
|
101748
101884
|
}
|
|
101749
101885
|
}, data.timeoutMs);
|
|
101750
|
-
pendingApprovalTimeouts.set(shortKey, timer);
|
|
101886
|
+
state.pendingApprovalTimeouts.set(shortKey, timer);
|
|
101751
101887
|
}
|
|
101752
101888
|
callbacks.trackOutbound();
|
|
101753
101889
|
return { success: true, durationMs: Date.now() - startTime };
|
|
@@ -101913,6 +102049,8 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
|
|
|
101913
102049
|
reconnectAttempts = 0;
|
|
101914
102050
|
reconnectTimer = null;
|
|
101915
102051
|
responseBuffers = /* @__PURE__ */ new Map();
|
|
102052
|
+
/** Instance-scoped outbound state — prevents cross-adapter leakage when multiInstance: true. */
|
|
102053
|
+
outboundState = createTelegramOutboundState();
|
|
101916
102054
|
constructor(id, config, displayName = "Telegram") {
|
|
101917
102055
|
super(id, SUBJECT_PREFIX, displayName);
|
|
101918
102056
|
this.config = config;
|
|
@@ -101935,14 +102073,14 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
|
|
|
101935
102073
|
bot.on("callback_query:data", async (ctx) => {
|
|
101936
102074
|
try {
|
|
101937
102075
|
const data = JSON.parse(ctx.callbackQuery.data);
|
|
101938
|
-
const entry = callbackIdMap.get(data.k);
|
|
102076
|
+
const entry = this.outboundState.callbackIdMap.get(data.k);
|
|
101939
102077
|
if (!entry) {
|
|
101940
102078
|
await ctx.answerCallbackQuery({ text: "This approval has expired." });
|
|
101941
102079
|
return;
|
|
101942
102080
|
}
|
|
101943
102081
|
const approved = data.a === 1;
|
|
101944
|
-
callbackIdMap.delete(data.k);
|
|
101945
|
-
clearApprovalTimeout(data.k);
|
|
102082
|
+
this.outboundState.callbackIdMap.delete(data.k);
|
|
102083
|
+
clearApprovalTimeout(this.outboundState, data.k);
|
|
101946
102084
|
const opts = { from: `telegram:${ctx.from.id}` };
|
|
101947
102085
|
await relay.publish(`relay.system.approval.${entry.agentId}`, {
|
|
101948
102086
|
type: "approval_response",
|
|
@@ -101968,7 +102106,7 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
|
|
|
101968
102106
|
this.bot = bot;
|
|
101969
102107
|
this.signalUnsub = relay.onSignal(`${SUBJECT_PREFIX}.>`, (subject, signal) => {
|
|
101970
102108
|
if (signal.type === "typing")
|
|
101971
|
-
void handleTypingSignal(this.bot, subject, signal.state);
|
|
102109
|
+
void handleTypingSignal(this.bot, subject, this.outboundState, signal.state);
|
|
101972
102110
|
});
|
|
101973
102111
|
if (this.config.mode === "webhook") {
|
|
101974
102112
|
this.webhookServer = await startWebhookMode(bot, this.id, this.config.webhookUrl, this.config.webhookPort, this.config.webhookSecret);
|
|
@@ -101986,7 +102124,11 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
|
|
|
101986
102124
|
this.signalUnsub();
|
|
101987
102125
|
this.signalUnsub = null;
|
|
101988
102126
|
}
|
|
101989
|
-
clearAllTypingIntervals();
|
|
102127
|
+
clearAllTypingIntervals(this.outboundState);
|
|
102128
|
+
for (const timer of this.outboundState.pendingApprovalTimeouts.values())
|
|
102129
|
+
clearTimeout(timer);
|
|
102130
|
+
this.outboundState.pendingApprovalTimeouts.clear();
|
|
102131
|
+
this.outboundState.callbackIdMap.clear();
|
|
101990
102132
|
if (this.bot) {
|
|
101991
102133
|
if (this.config.mode === "webhook") {
|
|
101992
102134
|
try {
|
|
@@ -102015,26 +102157,13 @@ var TelegramAdapter = class _TelegramAdapter extends BaseRelayAdapter {
|
|
|
102015
102157
|
envelope,
|
|
102016
102158
|
bot: this.bot,
|
|
102017
102159
|
responseBuffers: this.responseBuffers,
|
|
102160
|
+
state: this.outboundState,
|
|
102018
102161
|
callbacks: this.makeOutboundCallbacks(),
|
|
102019
102162
|
streaming: this.config.streaming ?? true,
|
|
102020
102163
|
logger: this.logger
|
|
102021
102164
|
});
|
|
102022
102165
|
}
|
|
102023
102166
|
// --- Private helpers ---
|
|
102024
|
-
/** Build callbacks for inbound message handling. */
|
|
102025
|
-
makeInboundCallbacks() {
|
|
102026
|
-
return {
|
|
102027
|
-
trackInbound: () => this.trackInbound(),
|
|
102028
|
-
recordError: (err) => this.recordError(err)
|
|
102029
|
-
};
|
|
102030
|
-
}
|
|
102031
|
-
/** Build callbacks for outbound message delivery. */
|
|
102032
|
-
makeOutboundCallbacks() {
|
|
102033
|
-
return {
|
|
102034
|
-
trackOutbound: () => this.trackOutbound(),
|
|
102035
|
-
recordError: (err) => this.recordError(err)
|
|
102036
|
-
};
|
|
102037
|
-
}
|
|
102038
102167
|
/** Start grammy bot in long-polling mode with eager token validation. */
|
|
102039
102168
|
async startPollingMode(bot) {
|
|
102040
102169
|
await bot.init();
|
|
@@ -102153,20 +102282,11 @@ Leave empty if no custom headers are needed.`
|
|
|
102153
102282
|
}
|
|
102154
102283
|
]
|
|
102155
102284
|
};
|
|
102156
|
-
var WebhookAdapter = class {
|
|
102157
|
-
id;
|
|
102158
|
-
subjectPrefix;
|
|
102159
|
-
displayName;
|
|
102285
|
+
var WebhookAdapter = class extends BaseRelayAdapter {
|
|
102160
102286
|
config;
|
|
102161
|
-
relay = null;
|
|
102162
102287
|
/** Tracks nonces to prevent replay attacks. Maps `{adapterId}:{nonce}` -> expiresAt timestamp. */
|
|
102163
102288
|
nonceMap = /* @__PURE__ */ new Map();
|
|
102164
102289
|
nonceInterval = null;
|
|
102165
|
-
status = {
|
|
102166
|
-
state: "disconnected",
|
|
102167
|
-
messageCount: { inbound: 0, outbound: 0 },
|
|
102168
|
-
errorCount: 0
|
|
102169
|
-
};
|
|
102170
102290
|
/**
|
|
102171
102291
|
* Create a new WebhookAdapter instance.
|
|
102172
102292
|
*
|
|
@@ -102175,50 +102295,33 @@ var WebhookAdapter = class {
|
|
|
102175
102295
|
* @param displayName - Human-readable name (defaults to `Webhook ({id})`)
|
|
102176
102296
|
*/
|
|
102177
102297
|
constructor(id, config, displayName) {
|
|
102178
|
-
|
|
102298
|
+
super(id, config.inbound.subject, displayName ?? `Webhook (${id})`);
|
|
102179
102299
|
this.config = config;
|
|
102180
|
-
this.subjectPrefix = config.inbound.subject;
|
|
102181
|
-
this.displayName = displayName ?? `Webhook (${id})`;
|
|
102182
102300
|
}
|
|
102183
102301
|
/**
|
|
102184
|
-
*
|
|
102302
|
+
* Connect hook — begin nonce pruning interval.
|
|
102185
102303
|
*
|
|
102186
102304
|
* This adapter has no external connection to establish; it relies on the
|
|
102187
102305
|
* Express route calling `handleInbound()` for inbound messages. The nonce
|
|
102188
102306
|
* pruning interval is started here to prevent unbounded memory growth.
|
|
102307
|
+
* The relay publisher reference is stored by {@link BaseRelayAdapter.start}.
|
|
102189
102308
|
*
|
|
102190
|
-
*
|
|
102191
|
-
*
|
|
102192
|
-
* @param relay - The RelayPublisher to publish inbound messages to
|
|
102309
|
+
* @param _relay - The RelayPublisher (stored by base class; unused here)
|
|
102193
102310
|
*/
|
|
102194
|
-
async
|
|
102195
|
-
if (this.status.state === "connected")
|
|
102196
|
-
return;
|
|
102197
|
-
this.relay = relay;
|
|
102198
|
-
this.status = {
|
|
102199
|
-
...this.status,
|
|
102200
|
-
state: "connected",
|
|
102201
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
102202
|
-
};
|
|
102311
|
+
async _start(_relay) {
|
|
102203
102312
|
this.nonceInterval = setInterval(() => {
|
|
102204
102313
|
this.pruneExpiredNonces();
|
|
102205
102314
|
}, NONCE_PRUNE_INTERVAL_MS);
|
|
102206
102315
|
}
|
|
102207
102316
|
/**
|
|
102208
|
-
*
|
|
102209
|
-
*
|
|
102210
|
-
* Idempotent: safe to call multiple times.
|
|
102317
|
+
* Disconnect hook — clear nonce state and pruning interval.
|
|
102211
102318
|
*/
|
|
102212
|
-
async
|
|
102213
|
-
if (this.status.state === "disconnected")
|
|
102214
|
-
return;
|
|
102319
|
+
async _stop() {
|
|
102215
102320
|
if (this.nonceInterval !== null) {
|
|
102216
102321
|
clearInterval(this.nonceInterval);
|
|
102217
102322
|
this.nonceInterval = null;
|
|
102218
102323
|
}
|
|
102219
102324
|
this.nonceMap.clear();
|
|
102220
|
-
this.relay = null;
|
|
102221
|
-
this.status = { ...this.status, state: "disconnected" };
|
|
102222
102325
|
}
|
|
102223
102326
|
/**
|
|
102224
102327
|
* Handle an inbound webhook HTTP POST request.
|
|
@@ -102261,24 +102364,18 @@ var WebhookAdapter = class {
|
|
|
102261
102364
|
metadata: { platform: "webhook", adapterId: this.id, nonce },
|
|
102262
102365
|
responseContext: { platform: "webhook" }
|
|
102263
102366
|
};
|
|
102264
|
-
await this.relay.publish(this.config.inbound.subject, payload, {
|
|
102367
|
+
const result2 = await this.relay.publish(this.config.inbound.subject, payload, {
|
|
102265
102368
|
from: `relay.webhook.${this.id}`
|
|
102266
102369
|
});
|
|
102267
|
-
|
|
102268
|
-
|
|
102269
|
-
|
|
102270
|
-
|
|
102271
|
-
|
|
102272
|
-
|
|
102273
|
-
};
|
|
102370
|
+
if (result2.deliveredTo === 0 && result2.rejected?.length) {
|
|
102371
|
+
const reason = result2.rejected[0]?.reason ?? "unknown";
|
|
102372
|
+
this.recordError(new Error(`Publish rejected: ${reason}`));
|
|
102373
|
+
return { ok: false, error: `Publish rejected: ${reason}` };
|
|
102374
|
+
}
|
|
102375
|
+
this.trackInbound();
|
|
102274
102376
|
return { ok: true };
|
|
102275
102377
|
} catch (err) {
|
|
102276
|
-
this.
|
|
102277
|
-
...this.status,
|
|
102278
|
-
errorCount: this.status.errorCount + 1,
|
|
102279
|
-
lastError: err instanceof Error ? err.message : String(err),
|
|
102280
|
-
lastErrorAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
102281
|
-
};
|
|
102378
|
+
this.recordError(err);
|
|
102282
102379
|
return { ok: false, error: "Publish failed" };
|
|
102283
102380
|
}
|
|
102284
102381
|
}
|
|
@@ -102313,41 +102410,17 @@ var WebhookAdapter = class {
|
|
|
102313
102410
|
});
|
|
102314
102411
|
if (!response.ok) {
|
|
102315
102412
|
const error = `Outbound delivery failed: HTTP ${response.status}`;
|
|
102316
|
-
this.
|
|
102317
|
-
...this.status,
|
|
102318
|
-
errorCount: this.status.errorCount + 1,
|
|
102319
|
-
lastError: error,
|
|
102320
|
-
lastErrorAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
102321
|
-
};
|
|
102413
|
+
this.recordError(error);
|
|
102322
102414
|
return { success: false, error, durationMs: Date.now() - startTime };
|
|
102323
102415
|
}
|
|
102324
|
-
this.
|
|
102325
|
-
...this.status,
|
|
102326
|
-
messageCount: {
|
|
102327
|
-
...this.status.messageCount,
|
|
102328
|
-
outbound: this.status.messageCount.outbound + 1
|
|
102329
|
-
}
|
|
102330
|
-
};
|
|
102416
|
+
this.trackOutbound();
|
|
102331
102417
|
return { success: true, durationMs: Date.now() - startTime };
|
|
102332
102418
|
} catch (err) {
|
|
102333
102419
|
const error = err instanceof Error ? err.message : String(err);
|
|
102334
|
-
this.
|
|
102335
|
-
...this.status,
|
|
102336
|
-
errorCount: this.status.errorCount + 1,
|
|
102337
|
-
lastError: error,
|
|
102338
|
-
lastErrorAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
102339
|
-
};
|
|
102420
|
+
this.recordError(err);
|
|
102340
102421
|
return { success: false, error, durationMs: Date.now() - startTime };
|
|
102341
102422
|
}
|
|
102342
102423
|
}
|
|
102343
|
-
/**
|
|
102344
|
-
* Get the current adapter status.
|
|
102345
|
-
*
|
|
102346
|
-
* Returns a shallow copy to prevent external mutation of internal state.
|
|
102347
|
-
*/
|
|
102348
|
-
getStatus() {
|
|
102349
|
-
return { ...this.status };
|
|
102350
|
-
}
|
|
102351
102424
|
/** Remove expired nonces from the in-memory map. */
|
|
102352
102425
|
pruneExpiredNonces() {
|
|
102353
102426
|
const now = Date.now();
|
|
@@ -102391,6 +102464,15 @@ var SUBJECT_PREFIX2 = "relay.human.slack";
|
|
|
102391
102464
|
var GROUP_SEGMENT2 = "group";
|
|
102392
102465
|
var MAX_MESSAGE_LENGTH2 = 4e3;
|
|
102393
102466
|
var MAX_CONTENT_LENGTH2 = 32768;
|
|
102467
|
+
var SLACK_FORMATTING_RULES = [
|
|
102468
|
+
"FORMATTING RULES (you MUST follow these):",
|
|
102469
|
+
"- Do NOT use Markdown tables (| col | col |). Slack cannot render them.",
|
|
102470
|
+
"- For structured data: use bullet points, numbered lists, or bold key-value pairs.",
|
|
102471
|
+
'- Example: instead of a table, write "*Name*: Alice\\n*Role*: Engineer"',
|
|
102472
|
+
"- Use *bold* (single asterisk), _italic_ (underscore), `code`, ```code blocks```.",
|
|
102473
|
+
"- Do NOT use ## headings \u2014 Slack ignores them. Use *bold text* for section titles.",
|
|
102474
|
+
`- Keep responses concise. Slack messages over ${MAX_MESSAGE_LENGTH2} characters are truncated.`
|
|
102475
|
+
].join("\n");
|
|
102394
102476
|
var SKIP_SUBTYPES = /* @__PURE__ */ new Set([
|
|
102395
102477
|
"channel_join",
|
|
102396
102478
|
"channel_leave",
|
|
@@ -102483,7 +102565,7 @@ function clearCaches() {
|
|
|
102483
102565
|
userNameCache.clear();
|
|
102484
102566
|
channelNameCache.clear();
|
|
102485
102567
|
}
|
|
102486
|
-
async function handleInboundMessage2(event, client, relay, botUserId, callbacks, logger3 = noopLogger) {
|
|
102568
|
+
async function handleInboundMessage2(event, client, relay, botUserId, callbacks, logger3 = noopLogger, typingIndicator = "none", pendingReactions) {
|
|
102487
102569
|
if (event.user === botUserId) {
|
|
102488
102570
|
logger3.debug(`inbound skipped: echo (own user ${botUserId})`);
|
|
102489
102571
|
return;
|
|
@@ -102514,7 +102596,8 @@ async function handleInboundMessage2(event, client, relay, botUserId, callbacks,
|
|
|
102514
102596
|
platform: "slack",
|
|
102515
102597
|
maxLength: MAX_MESSAGE_LENGTH2,
|
|
102516
102598
|
supportedFormats: ["text", "mrkdwn"],
|
|
102517
|
-
instructions: `Reply to subject ${subject} to respond to this Slack message
|
|
102599
|
+
instructions: `Reply to subject ${subject} to respond to this Slack message.`,
|
|
102600
|
+
formattingInstructions: SLACK_FORMATTING_RULES
|
|
102518
102601
|
},
|
|
102519
102602
|
platformData: {
|
|
102520
102603
|
channelId: event.channel,
|
|
@@ -102525,47 +102608,67 @@ async function handleInboundMessage2(event, client, relay, botUserId, callbacks,
|
|
|
102525
102608
|
}
|
|
102526
102609
|
};
|
|
102527
102610
|
try {
|
|
102528
|
-
await relay.publish(subject, payload, {
|
|
102611
|
+
const result2 = await relay.publish(subject, payload, {
|
|
102529
102612
|
from: `${SUBJECT_PREFIX2}.bot`,
|
|
102530
102613
|
replyTo: subject
|
|
102531
102614
|
});
|
|
102615
|
+
if (result2.deliveredTo === 0 && result2.rejected?.length) {
|
|
102616
|
+
const reason = result2.rejected[0]?.reason ?? "unknown";
|
|
102617
|
+
callbacks.recordError(new Error(`Publish rejected: ${reason}`));
|
|
102618
|
+
logger3.warn(`inbound publish rejected for ${event.channel}: ${reason}`);
|
|
102619
|
+
return;
|
|
102620
|
+
}
|
|
102532
102621
|
callbacks.trackInbound();
|
|
102533
102622
|
logger3.debug(`inbound from ${senderName} in ${event.channel}: "${content3.slice(0, 80)}${content3.length > 80 ? "\u2026" : ""}" (${content3.length} chars) \u2192 ${subject}`);
|
|
102623
|
+
if (typingIndicator === "reaction") {
|
|
102624
|
+
if (pendingReactions) {
|
|
102625
|
+
const queue2 = pendingReactions.get(event.channel) ?? [];
|
|
102626
|
+
queue2.push(event.ts);
|
|
102627
|
+
pendingReactions.set(event.channel, queue2);
|
|
102628
|
+
}
|
|
102629
|
+
client.reactions.add({ channel: event.channel, name: "hourglass_flowing_sand", timestamp: event.ts }).then(() => {
|
|
102630
|
+
logger3.debug(`inbound: added typing reaction to ${event.channel}:${event.ts}`);
|
|
102631
|
+
}).catch((err) => {
|
|
102632
|
+
if (pendingReactions) {
|
|
102633
|
+
const queue2 = pendingReactions.get(event.channel);
|
|
102634
|
+
if (queue2) {
|
|
102635
|
+
const idx = queue2.indexOf(event.ts);
|
|
102636
|
+
if (idx !== -1)
|
|
102637
|
+
queue2.splice(idx, 1);
|
|
102638
|
+
if (queue2.length === 0)
|
|
102639
|
+
pendingReactions.delete(event.channel);
|
|
102640
|
+
}
|
|
102641
|
+
}
|
|
102642
|
+
logger3.warn(`inbound: failed to add typing reaction to ${event.channel}:${event.ts}: ${err instanceof Error ? err.message : String(err)}`);
|
|
102643
|
+
});
|
|
102644
|
+
}
|
|
102534
102645
|
} catch (err) {
|
|
102535
102646
|
callbacks.recordError(err);
|
|
102536
102647
|
logger3.warn(`inbound publish failed for ${event.channel}:`, err instanceof Error ? err.message : String(err));
|
|
102537
102648
|
}
|
|
102538
102649
|
}
|
|
102539
102650
|
|
|
102540
|
-
// ../relay/dist/adapters/slack/
|
|
102651
|
+
// ../relay/dist/adapters/slack/stream.js
|
|
102541
102652
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
102542
|
-
|
|
102543
|
-
|
|
102544
|
-
|
|
102545
|
-
|
|
102546
|
-
const entry = pendingApprovalTimeouts2.get(toolCallId);
|
|
102547
|
-
if (entry) {
|
|
102548
|
-
clearTimeout(entry.timer);
|
|
102549
|
-
pendingApprovalTimeouts2.delete(toolCallId);
|
|
102550
|
-
}
|
|
102653
|
+
|
|
102654
|
+
// ../relay/dist/adapters/slack/stream-api.js
|
|
102655
|
+
function asStreamClient(client) {
|
|
102656
|
+
return client;
|
|
102551
102657
|
}
|
|
102552
|
-
function
|
|
102553
|
-
|
|
102658
|
+
async function startStream(client, channel, threadTs) {
|
|
102659
|
+
const result2 = await asStreamClient(client).chat.startStream({ channel, thread_ts: threadTs });
|
|
102660
|
+
return result2.stream_id ?? "";
|
|
102554
102661
|
}
|
|
102555
|
-
function
|
|
102556
|
-
|
|
102557
|
-
if (payload === null || typeof payload !== "object")
|
|
102558
|
-
return void 0;
|
|
102559
|
-
const obj = payload;
|
|
102560
|
-
const pd = obj.platformData;
|
|
102561
|
-
if (!pd)
|
|
102562
|
-
return void 0;
|
|
102563
|
-
if (typeof pd.threadTs === "string" && pd.threadTs)
|
|
102564
|
-
return pd.threadTs;
|
|
102565
|
-
if (typeof pd.ts === "string" && pd.ts)
|
|
102566
|
-
return pd.ts;
|
|
102567
|
-
return void 0;
|
|
102662
|
+
async function appendStream(client, streamId, text6) {
|
|
102663
|
+
await asStreamClient(client).chat.appendStream({ stream_id: streamId, text: text6 });
|
|
102568
102664
|
}
|
|
102665
|
+
async function stopStream(client, streamId) {
|
|
102666
|
+
await asStreamClient(client).chat.stopStream({ stream_id: streamId });
|
|
102667
|
+
}
|
|
102668
|
+
|
|
102669
|
+
// ../relay/dist/adapters/slack/stream.js
|
|
102670
|
+
var STREAM_UPDATE_INTERVAL_MS = 1e3;
|
|
102671
|
+
var STREAM_TTL_MS = 5 * 60 * 1e3;
|
|
102569
102672
|
async function wrapSlackCall(fn, callbacks, startTime, trackDelivery = false) {
|
|
102570
102673
|
try {
|
|
102571
102674
|
await fn();
|
|
@@ -102581,28 +102684,50 @@ async function wrapSlackCall(fn, callbacks, startTime, trackDelivery = false) {
|
|
|
102581
102684
|
};
|
|
102582
102685
|
}
|
|
102583
102686
|
}
|
|
102584
|
-
function
|
|
102687
|
+
function buildStreamKey(channelId, streamKeyTs) {
|
|
102688
|
+
return streamKeyTs ? `${channelId}:${streamKeyTs}` : channelId;
|
|
102689
|
+
}
|
|
102690
|
+
function addTypingReaction(client, channelId, threadTs, typingIndicator, logger3) {
|
|
102585
102691
|
if (typingIndicator !== "reaction" || !threadTs)
|
|
102586
102692
|
return;
|
|
102587
|
-
void client.reactions.add({
|
|
102588
|
-
|
|
102589
|
-
|
|
102590
|
-
|
|
102591
|
-
|
|
102693
|
+
void client.reactions.add({ channel: channelId, name: "hourglass_flowing_sand", timestamp: threadTs }).catch((err) => {
|
|
102694
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102695
|
+
if (!msg.includes("already_reacted")) {
|
|
102696
|
+
logger3?.warn(`stream: failed to add typing reaction to ${channelId}:${threadTs}: ${msg}`);
|
|
102697
|
+
}
|
|
102592
102698
|
});
|
|
102593
102699
|
}
|
|
102594
|
-
function removeTypingReaction(client, channelId, threadTs, typingIndicator) {
|
|
102700
|
+
function removeTypingReaction(client, channelId, threadTs, typingIndicator, logger3) {
|
|
102595
102701
|
if (typingIndicator !== "reaction" || !threadTs)
|
|
102596
102702
|
return;
|
|
102597
|
-
void client.reactions.remove({
|
|
102598
|
-
|
|
102599
|
-
|
|
102600
|
-
|
|
102601
|
-
|
|
102703
|
+
void client.reactions.remove({ channel: channelId, name: "hourglass_flowing_sand", timestamp: threadTs }).catch((err) => {
|
|
102704
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102705
|
+
if (!msg.includes("no_reaction")) {
|
|
102706
|
+
logger3?.warn(`stream: failed to remove typing reaction from ${channelId}:${threadTs}: ${msg}`);
|
|
102707
|
+
}
|
|
102708
|
+
});
|
|
102709
|
+
}
|
|
102710
|
+
function removePendingReaction(client, channelId, typingIndicator, pendingReactions, logger3) {
|
|
102711
|
+
if (typingIndicator !== "reaction")
|
|
102712
|
+
return;
|
|
102713
|
+
const queue2 = pendingReactions.get(channelId);
|
|
102714
|
+
if (!queue2 || queue2.length === 0)
|
|
102715
|
+
return;
|
|
102716
|
+
const messageTs = queue2.shift();
|
|
102717
|
+
if (queue2.length === 0)
|
|
102718
|
+
pendingReactions.delete(channelId);
|
|
102719
|
+
void client.reactions.remove({ channel: channelId, name: "hourglass_flowing_sand", timestamp: messageTs }).then(() => {
|
|
102720
|
+
logger3?.debug?.(`stream: removed pending typing reaction from ${channelId}:${messageTs}`);
|
|
102721
|
+
}).catch((err) => {
|
|
102722
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102723
|
+
if (!msg.includes("no_reaction")) {
|
|
102724
|
+
logger3?.warn(`stream: failed to remove pending typing reaction from ${channelId}:${messageTs}: ${msg}`);
|
|
102725
|
+
}
|
|
102602
102726
|
});
|
|
102603
102727
|
}
|
|
102604
|
-
async function handleTextDelta(
|
|
102605
|
-
const
|
|
102728
|
+
async function handleTextDelta(textChunk, streaming, nativeStreaming, ctx) {
|
|
102729
|
+
const { channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs, logger: logger3 } = ctx;
|
|
102730
|
+
const key = buildStreamKey(channelId, streamKeyTs);
|
|
102606
102731
|
const existing = streamState.get(key);
|
|
102607
102732
|
if (!streaming) {
|
|
102608
102733
|
if (existing) {
|
|
@@ -102612,23 +102737,19 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
|
|
|
102612
102737
|
channelId,
|
|
102613
102738
|
threadTs: threadTs ?? "",
|
|
102614
102739
|
messageTs: "",
|
|
102615
|
-
// No message posted yet
|
|
102616
102740
|
accumulatedText: textChunk,
|
|
102617
102741
|
lastUpdateAt: 0,
|
|
102618
102742
|
startedAt: Date.now(),
|
|
102619
102743
|
streamId: randomUUID4()
|
|
102620
102744
|
});
|
|
102621
|
-
addTypingReaction(client, channelId, threadTs, typingIndicator);
|
|
102745
|
+
addTypingReaction(client, channelId, threadTs, typingIndicator, logger3);
|
|
102622
102746
|
}
|
|
102623
102747
|
return { success: true, durationMs: Date.now() - startTime };
|
|
102624
102748
|
}
|
|
102625
102749
|
if (existing) {
|
|
102626
102750
|
existing.accumulatedText += textChunk;
|
|
102627
102751
|
if (existing.nativeStreamId) {
|
|
102628
|
-
return wrapSlackCall(() => client.
|
|
102629
|
-
stream_id: existing.nativeStreamId,
|
|
102630
|
-
text: formatForPlatform(textChunk, "slack")
|
|
102631
|
-
}), callbacks, startTime);
|
|
102752
|
+
return wrapSlackCall(() => appendStream(client, existing.nativeStreamId, formatForPlatform(textChunk, "slack")), callbacks, startTime);
|
|
102632
102753
|
}
|
|
102633
102754
|
const now = Date.now();
|
|
102634
102755
|
if (now - existing.lastUpdateAt < STREAM_UPDATE_INTERVAL_MS) {
|
|
@@ -102636,49 +102757,38 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
|
|
|
102636
102757
|
}
|
|
102637
102758
|
existing.lastUpdateAt = now;
|
|
102638
102759
|
const formatted = formatForPlatform(existing.accumulatedText, "slack");
|
|
102639
|
-
const streamText = formatted.replace(/\n{2,}/g, "\n");
|
|
102640
102760
|
return wrapSlackCall(() => client.chat.update({
|
|
102641
102761
|
channel: channelId,
|
|
102642
102762
|
ts: existing.messageTs,
|
|
102643
|
-
text: truncateText(
|
|
102763
|
+
text: truncateText(formatted.replace(/\n{2,}/g, "\n"), MAX_MESSAGE_LENGTH2)
|
|
102644
102764
|
}), callbacks, startTime);
|
|
102645
102765
|
}
|
|
102646
102766
|
if (nativeStreaming && threadTs) {
|
|
102647
102767
|
try {
|
|
102648
|
-
const
|
|
102649
|
-
const result2 = await slackClient.chat.startStream({
|
|
102650
|
-
channel: channelId,
|
|
102651
|
-
thread_ts: threadTs
|
|
102652
|
-
});
|
|
102653
|
-
const nativeStreamId = result2.stream_id ?? "";
|
|
102768
|
+
const nativeStreamId = await startStream(client, channelId, threadTs);
|
|
102654
102769
|
const now = Date.now();
|
|
102655
102770
|
streamState.set(key, {
|
|
102656
102771
|
channelId,
|
|
102657
|
-
threadTs
|
|
102772
|
+
threadTs,
|
|
102658
102773
|
messageTs: "",
|
|
102659
|
-
// Not used in native streaming
|
|
102660
102774
|
accumulatedText: textChunk,
|
|
102661
102775
|
lastUpdateAt: now,
|
|
102662
102776
|
startedAt: now,
|
|
102663
102777
|
streamId: randomUUID4(),
|
|
102664
102778
|
nativeStreamId
|
|
102665
102779
|
});
|
|
102666
|
-
await
|
|
102667
|
-
|
|
102668
|
-
text: formatForPlatform(textChunk, "slack")
|
|
102669
|
-
});
|
|
102670
|
-
addTypingReaction(client, channelId, threadTs, typingIndicator);
|
|
102780
|
+
await appendStream(client, nativeStreamId, formatForPlatform(textChunk, "slack"));
|
|
102781
|
+
addTypingReaction(client, channelId, threadTs, typingIndicator, logger3);
|
|
102671
102782
|
return { success: true, durationMs: Date.now() - startTime };
|
|
102672
102783
|
} catch (err) {
|
|
102673
102784
|
callbacks.recordError(err);
|
|
102674
102785
|
}
|
|
102675
102786
|
}
|
|
102676
102787
|
try {
|
|
102677
|
-
const mrkdwn = formatForPlatform(textChunk, "slack");
|
|
102678
102788
|
const now = Date.now();
|
|
102679
102789
|
const result2 = await client.chat.postMessage({
|
|
102680
102790
|
channel: channelId,
|
|
102681
|
-
text: truncateText(
|
|
102791
|
+
text: truncateText(formatForPlatform(textChunk, "slack"), MAX_MESSAGE_LENGTH2),
|
|
102682
102792
|
...threadTs ? { thread_ts: threadTs } : {}
|
|
102683
102793
|
});
|
|
102684
102794
|
streamState.set(key, {
|
|
@@ -102690,26 +102800,22 @@ async function handleTextDelta(channelId, textChunk, threadTs, client, streamSta
|
|
|
102690
102800
|
startedAt: now,
|
|
102691
102801
|
streamId: randomUUID4()
|
|
102692
102802
|
});
|
|
102693
|
-
addTypingReaction(client, channelId, threadTs, typingIndicator);
|
|
102803
|
+
addTypingReaction(client, channelId, threadTs, typingIndicator, logger3);
|
|
102694
102804
|
return { success: true, durationMs: now - startTime };
|
|
102695
102805
|
} catch (err) {
|
|
102696
102806
|
callbacks.recordError(err);
|
|
102697
|
-
return {
|
|
102698
|
-
success: false,
|
|
102699
|
-
error: err instanceof Error ? err.message : String(err),
|
|
102700
|
-
durationMs: Date.now() - startTime
|
|
102701
|
-
};
|
|
102807
|
+
return { success: false, error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - startTime };
|
|
102702
102808
|
}
|
|
102703
102809
|
}
|
|
102704
|
-
async function flushStreamBuffer(
|
|
102705
|
-
const
|
|
102810
|
+
async function flushStreamBuffer(ctx) {
|
|
102811
|
+
const { channelId, threadTs, client, streamState, callbacks, streamKeyTs } = ctx;
|
|
102812
|
+
const key = buildStreamKey(channelId, streamKeyTs);
|
|
102706
102813
|
const existing = streamState.get(key);
|
|
102707
102814
|
if (!existing || !existing.accumulatedText)
|
|
102708
102815
|
return;
|
|
102709
102816
|
if (existing.nativeStreamId) {
|
|
102710
|
-
const slackClient = client;
|
|
102711
102817
|
try {
|
|
102712
|
-
await
|
|
102818
|
+
await stopStream(client, existing.nativeStreamId);
|
|
102713
102819
|
} catch {
|
|
102714
102820
|
}
|
|
102715
102821
|
existing.nativeStreamId = void 0;
|
|
@@ -102740,19 +102846,21 @@ async function flushStreamBuffer(channelId, threadTs, client, streamState, callb
|
|
|
102740
102846
|
callbacks.recordError(err);
|
|
102741
102847
|
}
|
|
102742
102848
|
}
|
|
102743
|
-
async function handleDone(
|
|
102744
|
-
const
|
|
102849
|
+
async function handleDone(ctx) {
|
|
102850
|
+
const { channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs, pendingReactions, logger: logger3 } = ctx;
|
|
102851
|
+
const key = buildStreamKey(channelId, streamKeyTs);
|
|
102745
102852
|
const existing = streamState.get(key);
|
|
102746
102853
|
streamState.delete(key);
|
|
102854
|
+
removePendingReaction(client, channelId, typingIndicator, pendingReactions, logger3);
|
|
102747
102855
|
if (existing?.threadTs) {
|
|
102748
|
-
removeTypingReaction(client, channelId, existing.threadTs, typingIndicator);
|
|
102856
|
+
removeTypingReaction(client, channelId, existing.threadTs, typingIndicator, logger3);
|
|
102749
102857
|
}
|
|
102750
102858
|
if (!existing) {
|
|
102859
|
+
logger3?.warn(`stream: done received for ${channelId} with no active stream (empty response \u2014 user may see no output)`);
|
|
102751
102860
|
return { success: true, durationMs: Date.now() - startTime };
|
|
102752
102861
|
}
|
|
102753
102862
|
if (existing.nativeStreamId) {
|
|
102754
|
-
|
|
102755
|
-
return wrapSlackCall(() => slackClient.chat.stopStream({ stream_id: existing.nativeStreamId }), callbacks, startTime, true);
|
|
102863
|
+
return wrapSlackCall(() => stopStream(client, existing.nativeStreamId), callbacks, startTime, true);
|
|
102756
102864
|
}
|
|
102757
102865
|
if (!existing.messageTs) {
|
|
102758
102866
|
return wrapSlackCall(() => client.chat.postMessage({
|
|
@@ -102767,27 +102875,24 @@ async function handleDone(channelId, threadTs, client, streamState, callbacks, s
|
|
|
102767
102875
|
text: truncateText(formatForPlatform(existing.accumulatedText, "slack"), MAX_MESSAGE_LENGTH2)
|
|
102768
102876
|
}), callbacks, startTime, true);
|
|
102769
102877
|
}
|
|
102770
|
-
async function handleError(
|
|
102771
|
-
const
|
|
102878
|
+
async function handleError(errorMsg, ctx) {
|
|
102879
|
+
const { channelId, threadTs, client, streamState, callbacks, startTime, typingIndicator, streamKeyTs, pendingReactions, logger: logger3 } = ctx;
|
|
102880
|
+
const key = buildStreamKey(channelId, streamKeyTs);
|
|
102772
102881
|
const existing = streamState.get(key);
|
|
102773
102882
|
streamState.delete(key);
|
|
102883
|
+
removePendingReaction(client, channelId, typingIndicator, pendingReactions, logger3);
|
|
102774
102884
|
if (existing?.threadTs) {
|
|
102775
|
-
removeTypingReaction(client, channelId, existing.threadTs, typingIndicator);
|
|
102885
|
+
removeTypingReaction(client, channelId, existing.threadTs, typingIndicator, logger3);
|
|
102776
102886
|
}
|
|
102777
102887
|
if (existing) {
|
|
102778
102888
|
if (existing.nativeStreamId) {
|
|
102779
|
-
const slackClient = client;
|
|
102780
|
-
const errorSuffix = formatForPlatform(`
|
|
102781
|
-
|
|
102782
|
-
[Error: ${errorMsg}]`, "slack");
|
|
102783
102889
|
try {
|
|
102784
|
-
await
|
|
102785
|
-
|
|
102786
|
-
|
|
102787
|
-
});
|
|
102890
|
+
await appendStream(client, existing.nativeStreamId, formatForPlatform(`
|
|
102891
|
+
|
|
102892
|
+
[Error: ${errorMsg}]`, "slack"));
|
|
102788
102893
|
} catch {
|
|
102789
102894
|
}
|
|
102790
|
-
return wrapSlackCall(() =>
|
|
102895
|
+
return wrapSlackCall(() => stopStream(client, existing.nativeStreamId), callbacks, startTime, true);
|
|
102791
102896
|
}
|
|
102792
102897
|
const finalText = truncateText(`${formatForPlatform(existing.accumulatedText, "slack")}
|
|
102793
102898
|
|
|
@@ -102799,101 +102904,35 @@ async function handleError(channelId, errorMsg, threadTs, client, streamState, c
|
|
|
102799
102904
|
...threadTs ? { thread_ts: threadTs } : {}
|
|
102800
102905
|
}), callbacks, startTime, true);
|
|
102801
102906
|
}
|
|
102802
|
-
return wrapSlackCall(() => client.chat.update({
|
|
102803
|
-
channel: channelId,
|
|
102804
|
-
ts: existing.messageTs,
|
|
102805
|
-
text: finalText
|
|
102806
|
-
}), callbacks, startTime, true);
|
|
102907
|
+
return wrapSlackCall(() => client.chat.update({ channel: channelId, ts: existing.messageTs, text: finalText }), callbacks, startTime, true);
|
|
102807
102908
|
}
|
|
102808
|
-
const text6 = truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH2);
|
|
102809
102909
|
return wrapSlackCall(() => client.chat.postMessage({
|
|
102810
102910
|
channel: channelId,
|
|
102811
|
-
text:
|
|
102911
|
+
text: truncateText(`[Error: ${errorMsg}]`, MAX_MESSAGE_LENGTH2),
|
|
102812
102912
|
...threadTs ? { thread_ts: threadTs } : {}
|
|
102813
102913
|
}), callbacks, startTime, true);
|
|
102814
102914
|
}
|
|
102815
|
-
|
|
102816
|
-
|
|
102817
|
-
|
|
102818
|
-
|
|
102819
|
-
if (startTime - stream.startedAt > STREAM_TTL_MS) {
|
|
102820
|
-
streamState.delete(key);
|
|
102821
|
-
logger3.warn(`stream: reaped orphaned stream for ${key} (age: ${Math.round((startTime - stream.startedAt) / 1e3)}s)`);
|
|
102822
|
-
}
|
|
102823
|
-
}
|
|
102824
|
-
if (envelope.from.startsWith(SUBJECT_PREFIX2)) {
|
|
102825
|
-
logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
|
|
102826
|
-
return { success: true, durationMs: Date.now() - startTime };
|
|
102827
|
-
}
|
|
102828
|
-
if (!client) {
|
|
102829
|
-
return {
|
|
102830
|
-
success: false,
|
|
102831
|
-
error: `SlackAdapter(${adapterId}): not started`,
|
|
102832
|
-
durationMs: Date.now() - startTime
|
|
102833
|
-
};
|
|
102834
|
-
}
|
|
102835
|
-
const channelId = extractChannelId(subject);
|
|
102836
|
-
if (!channelId) {
|
|
102837
|
-
return {
|
|
102838
|
-
success: false,
|
|
102839
|
-
error: `SlackAdapter(${adapterId}): cannot extract channel ID from subject '${subject}'`,
|
|
102840
|
-
durationMs: Date.now() - startTime
|
|
102841
|
-
};
|
|
102842
|
-
}
|
|
102843
|
-
const threadTs = resolveThreadTs(envelope);
|
|
102844
|
-
const payloadObj = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : void 0;
|
|
102845
|
-
const payloadCorrelationId = payloadObj?.correlationId;
|
|
102846
|
-
const streamKeyTs = threadTs ?? payloadCorrelationId ?? envelope.from;
|
|
102847
|
-
const eventType = detectStreamEventType(envelope.payload);
|
|
102848
|
-
if (eventType) {
|
|
102849
|
-
const textChunk = extractTextDelta(envelope.payload);
|
|
102850
|
-
if (textChunk) {
|
|
102851
|
-
logger3.debug(`deliver: text_delta to ${channelId} (${textChunk.length} chars, streaming=${opts.streaming ? opts.nativeStreaming ? "native" : "legacy" : "buffered"})`);
|
|
102852
|
-
return handleTextDelta(channelId, textChunk, threadTs, client, streamState, callbacks, startTime, opts.streaming, opts.nativeStreaming, opts.typingIndicator, streamKeyTs);
|
|
102853
|
-
}
|
|
102854
|
-
const errorMsg = extractErrorMessage(envelope.payload);
|
|
102855
|
-
if (errorMsg) {
|
|
102856
|
-
logger3.debug(`deliver: error to ${channelId}: "${errorMsg.slice(0, 100)}"`);
|
|
102857
|
-
return handleError(channelId, errorMsg, threadTs, client, streamState, callbacks, startTime, opts.typingIndicator, streamKeyTs);
|
|
102858
|
-
}
|
|
102859
|
-
if (eventType === "done") {
|
|
102860
|
-
logger3.debug(`deliver: done for ${channelId}`);
|
|
102861
|
-
return handleDone(channelId, threadTs, client, streamState, callbacks, startTime, opts.typingIndicator, streamKeyTs);
|
|
102862
|
-
}
|
|
102863
|
-
if (eventType === "approval_required") {
|
|
102864
|
-
const approvalData = extractApprovalData(envelope.payload);
|
|
102865
|
-
if (approvalData) {
|
|
102866
|
-
logger3.debug(`deliver: approval_required for tool '${approvalData.toolName}' to ${channelId}`);
|
|
102867
|
-
await flushStreamBuffer(channelId, threadTs, client, streamState, callbacks, streamKeyTs);
|
|
102868
|
-
return handleApprovalRequired2(channelId, threadTs, approvalData, envelope, client, callbacks, startTime);
|
|
102869
|
-
}
|
|
102870
|
-
}
|
|
102871
|
-
logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
|
|
102872
|
-
return { success: true, durationMs: Date.now() - startTime };
|
|
102873
|
-
}
|
|
102874
|
-
const content3 = extractPayloadContent(envelope.payload);
|
|
102875
|
-
const mrkdwn = formatForPlatform(content3, "slack");
|
|
102876
|
-
const text6 = truncateText(mrkdwn, MAX_MESSAGE_LENGTH2);
|
|
102877
|
-
logger3.debug(`deliver: standard payload to ${channelId} (${text6.length} chars)`);
|
|
102878
|
-
return wrapSlackCall(() => client.chat.postMessage({
|
|
102879
|
-
channel: channelId,
|
|
102880
|
-
text: text6,
|
|
102881
|
-
...threadTs ? { thread_ts: threadTs } : {}
|
|
102882
|
-
}), callbacks, startTime, true);
|
|
102915
|
+
|
|
102916
|
+
// ../relay/dist/adapters/slack/approval.js
|
|
102917
|
+
function createSlackOutboundState() {
|
|
102918
|
+
return { pendingApprovalTimeouts: /* @__PURE__ */ new Map() };
|
|
102883
102919
|
}
|
|
102884
|
-
function
|
|
102885
|
-
const
|
|
102886
|
-
|
|
102887
|
-
|
|
102920
|
+
function clearAllApprovalTimeouts(state) {
|
|
102921
|
+
for (const entry of state.pendingApprovalTimeouts.values()) {
|
|
102922
|
+
clearTimeout(entry.timer);
|
|
102923
|
+
}
|
|
102924
|
+
state.pendingApprovalTimeouts.clear();
|
|
102888
102925
|
}
|
|
102889
|
-
function
|
|
102890
|
-
const
|
|
102891
|
-
|
|
102892
|
-
|
|
102926
|
+
function clearApprovalTimeout2(state, toolCallId) {
|
|
102927
|
+
const entry = state.pendingApprovalTimeouts.get(toolCallId);
|
|
102928
|
+
if (entry) {
|
|
102929
|
+
clearTimeout(entry.timer);
|
|
102930
|
+
state.pendingApprovalTimeouts.delete(toolCallId);
|
|
102931
|
+
}
|
|
102893
102932
|
}
|
|
102894
|
-
async function handleApprovalRequired2(channelId, threadTs, data, envelope, client, callbacks, startTime) {
|
|
102895
|
-
const agentId =
|
|
102896
|
-
const sessionId =
|
|
102933
|
+
async function handleApprovalRequired2(channelId, threadTs, data, envelope, client, callbacks, startTime, state) {
|
|
102934
|
+
const agentId = extractAgentIdFromEnvelope(envelope) ?? "unknown";
|
|
102935
|
+
const sessionId = extractSessionIdFromEnvelope(envelope) ?? "unknown";
|
|
102897
102936
|
const inputPreview = truncateText(data.input, 500);
|
|
102898
102937
|
const toolDescription = formatToolDescription(data.toolName, data.input);
|
|
102899
102938
|
const buttonValue = JSON.stringify({
|
|
@@ -102952,7 +102991,7 @@ ${inputPreview}
|
|
|
102952
102991
|
if (result2.success && postedTs && data.timeoutMs > 0) {
|
|
102953
102992
|
const msgTs = postedTs;
|
|
102954
102993
|
const timer = setTimeout(async () => {
|
|
102955
|
-
|
|
102994
|
+
state.pendingApprovalTimeouts.delete(data.toolCallId);
|
|
102956
102995
|
try {
|
|
102957
102996
|
await client.chat.update({
|
|
102958
102997
|
channel: channelId,
|
|
@@ -102968,7 +103007,7 @@ ${inputPreview}
|
|
|
102968
103007
|
} catch {
|
|
102969
103008
|
}
|
|
102970
103009
|
}, data.timeoutMs);
|
|
102971
|
-
|
|
103010
|
+
state.pendingApprovalTimeouts.set(data.toolCallId, {
|
|
102972
103011
|
timer,
|
|
102973
103012
|
channelId,
|
|
102974
103013
|
messageTs: msgTs,
|
|
@@ -102978,24 +103017,107 @@ ${inputPreview}
|
|
|
102978
103017
|
return result2;
|
|
102979
103018
|
}
|
|
102980
103019
|
|
|
103020
|
+
// ../relay/dist/adapters/slack/outbound.js
|
|
103021
|
+
function resolveThreadTs(envelope) {
|
|
103022
|
+
const payload = envelope.payload;
|
|
103023
|
+
if (payload === null || typeof payload !== "object")
|
|
103024
|
+
return void 0;
|
|
103025
|
+
const obj = payload;
|
|
103026
|
+
const pd = obj.platformData;
|
|
103027
|
+
if (!pd)
|
|
103028
|
+
return void 0;
|
|
103029
|
+
if (typeof pd.threadTs === "string" && pd.threadTs)
|
|
103030
|
+
return pd.threadTs;
|
|
103031
|
+
if (typeof pd.ts === "string" && pd.ts)
|
|
103032
|
+
return pd.ts;
|
|
103033
|
+
return void 0;
|
|
103034
|
+
}
|
|
103035
|
+
async function deliverMessage2(opts) {
|
|
103036
|
+
const { adapterId, subject, envelope, client, streamState, pendingReactions, callbacks, logger: logger3 = noopLogger } = opts;
|
|
103037
|
+
const startTime = Date.now();
|
|
103038
|
+
for (const [key, stream] of streamState) {
|
|
103039
|
+
if (startTime - stream.startedAt > STREAM_TTL_MS) {
|
|
103040
|
+
streamState.delete(key);
|
|
103041
|
+
logger3.warn(`stream: reaped orphaned stream for ${key} (age: ${Math.round((startTime - stream.startedAt) / 1e3)}s)`);
|
|
103042
|
+
}
|
|
103043
|
+
}
|
|
103044
|
+
if (envelope.from.startsWith(SUBJECT_PREFIX2)) {
|
|
103045
|
+
logger3.debug("deliver: echo prevention \u2014 skipping self-originated message");
|
|
103046
|
+
return { success: true, durationMs: Date.now() - startTime };
|
|
103047
|
+
}
|
|
103048
|
+
if (!client) {
|
|
103049
|
+
return { success: false, error: `SlackAdapter(${adapterId}): not started`, durationMs: Date.now() - startTime };
|
|
103050
|
+
}
|
|
103051
|
+
const channelId = extractChannelId(subject);
|
|
103052
|
+
if (!channelId) {
|
|
103053
|
+
return {
|
|
103054
|
+
success: false,
|
|
103055
|
+
error: `SlackAdapter(${adapterId}): cannot extract channel ID from subject '${subject}'`,
|
|
103056
|
+
durationMs: Date.now() - startTime
|
|
103057
|
+
};
|
|
103058
|
+
}
|
|
103059
|
+
const threadTs = resolveThreadTs(envelope);
|
|
103060
|
+
const payloadObj = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : void 0;
|
|
103061
|
+
const streamKeyTs = threadTs ?? payloadObj?.correlationId ?? envelope.from;
|
|
103062
|
+
const ctx = {
|
|
103063
|
+
channelId,
|
|
103064
|
+
threadTs,
|
|
103065
|
+
client,
|
|
103066
|
+
streamState,
|
|
103067
|
+
callbacks,
|
|
103068
|
+
startTime,
|
|
103069
|
+
typingIndicator: opts.typingIndicator,
|
|
103070
|
+
streamKeyTs,
|
|
103071
|
+
pendingReactions,
|
|
103072
|
+
logger: logger3
|
|
103073
|
+
};
|
|
103074
|
+
const eventType = detectStreamEventType(envelope.payload);
|
|
103075
|
+
if (eventType) {
|
|
103076
|
+
const textChunk = extractTextDelta(envelope.payload);
|
|
103077
|
+
if (textChunk) {
|
|
103078
|
+
logger3.debug(`deliver: text_delta to ${channelId} (${textChunk.length} chars, streaming=${opts.streaming ? opts.nativeStreaming ? "native" : "legacy" : "buffered"})`);
|
|
103079
|
+
return handleTextDelta(textChunk, opts.streaming, opts.nativeStreaming, ctx);
|
|
103080
|
+
}
|
|
103081
|
+
const errorMsg = extractErrorMessage(envelope.payload);
|
|
103082
|
+
if (errorMsg) {
|
|
103083
|
+
logger3.debug(`deliver: error to ${channelId}: "${errorMsg.slice(0, 100)}"`);
|
|
103084
|
+
return handleError(errorMsg, ctx);
|
|
103085
|
+
}
|
|
103086
|
+
if (eventType === "done") {
|
|
103087
|
+
logger3.debug(`deliver: done for ${channelId}`);
|
|
103088
|
+
return handleDone(ctx);
|
|
103089
|
+
}
|
|
103090
|
+
if (eventType === "approval_required") {
|
|
103091
|
+
const approvalData = extractApprovalData(envelope.payload);
|
|
103092
|
+
if (approvalData) {
|
|
103093
|
+
logger3.debug(`deliver: approval_required for tool '${approvalData.toolName}' to ${channelId}`);
|
|
103094
|
+
await flushStreamBuffer(ctx);
|
|
103095
|
+
return handleApprovalRequired2(channelId, threadTs, approvalData, envelope, client, callbacks, startTime, opts.approvalState);
|
|
103096
|
+
}
|
|
103097
|
+
}
|
|
103098
|
+
logger3.debug(`deliver: dropping stream event '${eventType}' (whitelist)`);
|
|
103099
|
+
return { success: true, durationMs: Date.now() - startTime };
|
|
103100
|
+
}
|
|
103101
|
+
const text6 = truncateText(formatForPlatform(extractPayloadContent(envelope.payload), "slack"), MAX_MESSAGE_LENGTH2);
|
|
103102
|
+
logger3.debug(`deliver: standard payload to ${channelId} (${text6.length} chars)`);
|
|
103103
|
+
return wrapSlackCall(() => client.chat.postMessage({ channel: channelId, text: text6, ...threadTs ? { thread_ts: threadTs } : {} }), callbacks, startTime, true);
|
|
103104
|
+
}
|
|
103105
|
+
|
|
102981
103106
|
// ../relay/dist/adapters/slack/slack-adapter.js
|
|
102982
103107
|
var SLACK_APP_MANIFEST_YAML = `display_information:
|
|
102983
103108
|
name: DorkOS Relay
|
|
102984
|
-
settings:
|
|
102985
|
-
socket_mode_enabled: true
|
|
102986
|
-
event_subscriptions:
|
|
102987
|
-
bot_events:
|
|
102988
|
-
- message.channels
|
|
102989
|
-
- message.groups
|
|
102990
|
-
- message.im
|
|
102991
|
-
- app_mention
|
|
102992
103109
|
features:
|
|
103110
|
+
app_home:
|
|
103111
|
+
home_tab_enabled: false
|
|
103112
|
+
messages_tab_enabled: true
|
|
103113
|
+
messages_tab_read_only_enabled: false
|
|
102993
103114
|
bot_user:
|
|
102994
103115
|
display_name: DorkOS Relay
|
|
102995
103116
|
always_online: false
|
|
102996
103117
|
oauth_config:
|
|
102997
103118
|
scopes:
|
|
102998
103119
|
bot:
|
|
103120
|
+
- app_mentions:read
|
|
102999
103121
|
- channels:history
|
|
103000
103122
|
- channels:read
|
|
103001
103123
|
- chat:write
|
|
@@ -103005,9 +103127,22 @@ oauth_config:
|
|
|
103005
103127
|
- im:read
|
|
103006
103128
|
- im:write
|
|
103007
103129
|
- mpim:history
|
|
103008
|
-
-
|
|
103130
|
+
- reactions:read
|
|
103131
|
+
- reactions:write
|
|
103009
103132
|
- users:read
|
|
103010
|
-
|
|
103133
|
+
settings:
|
|
103134
|
+
event_subscriptions:
|
|
103135
|
+
bot_events:
|
|
103136
|
+
- app_mention
|
|
103137
|
+
- message.channels
|
|
103138
|
+
- message.groups
|
|
103139
|
+
- message.im
|
|
103140
|
+
- message.mpim
|
|
103141
|
+
interactivity:
|
|
103142
|
+
is_enabled: true
|
|
103143
|
+
org_deploy_enabled: false
|
|
103144
|
+
socket_mode_enabled: true
|
|
103145
|
+
token_rotation_enabled: false`;
|
|
103011
103146
|
var SLACK_CREATE_APP_URL = `https://api.slack.com/apps?new_app=1&manifest_yaml=${encodeURIComponent(SLACK_APP_MANIFEST_YAML)}`;
|
|
103012
103147
|
var SLACK_MANIFEST = {
|
|
103013
103148
|
type: "slack",
|
|
@@ -103026,7 +103161,7 @@ var SLACK_MANIFEST = {
|
|
|
103026
103161
|
{
|
|
103027
103162
|
stepId: "create-app",
|
|
103028
103163
|
title: "Create & Configure a Slack App",
|
|
103029
|
-
description: 'Go to api.slack.com/apps \u2192 Create New App \u2192 From Scratch.\n\n1. **Socket Mode** \u2014 Enable it (Settings \u2192 Socket Mode).\n2. **Event Subscriptions** \u2014 Turn on Enable Events, then subscribe to bot events: message.channels, message.groups, message.im,
|
|
103164
|
+
description: 'Go to api.slack.com/apps \u2192 Create New App \u2192 From Scratch.\n\n1. **Socket Mode** \u2014 Enable it (Settings \u2192 Socket Mode).\n2. **Event Subscriptions** \u2014 Turn on Enable Events, then subscribe to bot events: app_mention, message.channels, message.groups, message.im, message.mpim.\n3. **OAuth & Permissions** \u2014 Add bot token scopes: app_mentions:read, channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, reactions:read, reactions:write, users:read. Then install the app to your workspace.\n4. **App-Level Token** \u2014 In Basic Information \u2192 App-Level Tokens, generate a token with the connections:write scope.\n\n\u26A0\uFE0F Do NOT enable "Agents & AI Apps" \u2014 it adds user scopes that cause install failures on most workspaces.',
|
|
103030
103165
|
fields: ["botToken", "appToken", "signingSecret", "streaming", "nativeStreaming", "typingIndicator"]
|
|
103031
103166
|
}
|
|
103032
103167
|
],
|
|
@@ -103106,10 +103241,10 @@ var SLACK_MANIFEST = {
|
|
|
103106
103241
|
{ label: "Emoji reaction", value: "reaction" }
|
|
103107
103242
|
],
|
|
103108
103243
|
visibleByDefault: true,
|
|
103109
|
-
helpMarkdown: 'When set to "Emoji reaction", adds an :hourglass_flowing_sand: reaction to your message while the agent is processing. Requires the `reactions:write`
|
|
103244
|
+
helpMarkdown: 'When set to "Emoji reaction", adds an :hourglass_flowing_sand: reaction to your message while the agent is processing. Requires the `reactions:write` and `reactions:read` scopes.'
|
|
103110
103245
|
}
|
|
103111
103246
|
],
|
|
103112
|
-
setupInstructions: '1. Create a Slack app at api.slack.com/apps (From Scratch, not From Manifest).\n2. Enable Socket Mode (Settings \u2192 Socket Mode).\n3. Enable Event Subscriptions and subscribe to bot events: message.channels, message.groups, message.im,
|
|
103247
|
+
setupInstructions: '1. Create a Slack app at api.slack.com/apps (From Scratch, not From Manifest).\n2. Enable Socket Mode (Settings \u2192 Socket Mode).\n3. Enable Event Subscriptions and subscribe to bot events: app_mention, message.channels, message.groups, message.im, message.mpim.\n4. Add bot token scopes under OAuth & Permissions: app_mentions:read, channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, reactions:read, reactions:write, users:read.\n5. Install the app to your workspace (OAuth & Permissions \u2192 Install).\n6. Copy the Bot User OAuth Token (starts with xoxb-).\n7. Generate an App-Level Token with connections:write scope (Basic Information \u2192 App-Level Tokens).\n8. Copy the Signing Secret from Basic Information.\n\n\u26A0\uFE0F Do NOT enable "Agents & AI Apps" \u2014 it adds user-level scopes that cause invalid_scope errors on most workspace plans.'
|
|
103113
103248
|
};
|
|
103114
103249
|
var SlackAdapter = class extends BaseRelayAdapter {
|
|
103115
103250
|
config;
|
|
@@ -103117,6 +103252,9 @@ var SlackAdapter = class extends BaseRelayAdapter {
|
|
|
103117
103252
|
/** Bot's own user ID — cached after auth.test for echo prevention. */
|
|
103118
103253
|
botUserId = "";
|
|
103119
103254
|
streamState = /* @__PURE__ */ new Map();
|
|
103255
|
+
/** FIFO queue of message timestamps with pending hourglass reactions, keyed by channelId. */
|
|
103256
|
+
pendingReactions = /* @__PURE__ */ new Map();
|
|
103257
|
+
outboundState = createSlackOutboundState();
|
|
103120
103258
|
constructor(id, config, displayName = "Slack") {
|
|
103121
103259
|
super(id, SUBJECT_PREFIX2, displayName);
|
|
103122
103260
|
this.config = config;
|
|
@@ -103149,10 +103287,10 @@ var SlackAdapter = class extends BaseRelayAdapter {
|
|
|
103149
103287
|
const authResult = await app.client.auth.test();
|
|
103150
103288
|
this.botUserId = authResult.user_id ?? "";
|
|
103151
103289
|
app.message(async ({ event, client }) => {
|
|
103152
|
-
await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger);
|
|
103290
|
+
await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger, this.config.typingIndicator ?? "none", this.pendingReactions);
|
|
103153
103291
|
});
|
|
103154
103292
|
app.event("app_mention", async ({ event, client }) => {
|
|
103155
|
-
await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger);
|
|
103293
|
+
await handleInboundMessage2(event, client, relay, this.botUserId, this.makeInboundCallbacks(), this.logger, this.config.typingIndicator ?? "none", this.pendingReactions);
|
|
103156
103294
|
});
|
|
103157
103295
|
app.action("tool_approve", async ({ ack, action, body, client }) => {
|
|
103158
103296
|
await ack();
|
|
@@ -103179,6 +103317,8 @@ var SlackAdapter = class extends BaseRelayAdapter {
|
|
|
103179
103317
|
}
|
|
103180
103318
|
this.botUserId = "";
|
|
103181
103319
|
this.streamState.clear();
|
|
103320
|
+
this.pendingReactions.clear();
|
|
103321
|
+
clearAllApprovalTimeouts(this.outboundState);
|
|
103182
103322
|
clearCaches();
|
|
103183
103323
|
}
|
|
103184
103324
|
/**
|
|
@@ -103197,28 +103337,16 @@ var SlackAdapter = class extends BaseRelayAdapter {
|
|
|
103197
103337
|
envelope,
|
|
103198
103338
|
client: this.app?.client ?? null,
|
|
103199
103339
|
streamState: this.streamState,
|
|
103340
|
+
pendingReactions: this.pendingReactions,
|
|
103200
103341
|
botUserId: this.botUserId,
|
|
103201
103342
|
callbacks: this.makeOutboundCallbacks(),
|
|
103202
103343
|
streaming: this.config.streaming ?? true,
|
|
103203
103344
|
nativeStreaming: this.config.nativeStreaming ?? true,
|
|
103204
103345
|
typingIndicator: this.config.typingIndicator ?? "none",
|
|
103346
|
+
approvalState: this.outboundState,
|
|
103205
103347
|
logger: this.logger
|
|
103206
103348
|
});
|
|
103207
103349
|
}
|
|
103208
|
-
/** Build callbacks for inbound message handling. */
|
|
103209
|
-
makeInboundCallbacks() {
|
|
103210
|
-
return {
|
|
103211
|
-
trackInbound: () => this.trackInbound(),
|
|
103212
|
-
recordError: (err) => this.recordError(err)
|
|
103213
|
-
};
|
|
103214
|
-
}
|
|
103215
|
-
/** Build callbacks for outbound message delivery. */
|
|
103216
|
-
makeOutboundCallbacks() {
|
|
103217
|
-
return {
|
|
103218
|
-
trackOutbound: () => this.trackOutbound(),
|
|
103219
|
-
recordError: (err) => this.recordError(err)
|
|
103220
|
-
};
|
|
103221
|
-
}
|
|
103222
103350
|
/**
|
|
103223
103351
|
* Handle a tool approval or denial action from Slack interactive buttons.
|
|
103224
103352
|
*
|
|
@@ -103240,7 +103368,7 @@ var SlackAdapter = class extends BaseRelayAdapter {
|
|
|
103240
103368
|
return;
|
|
103241
103369
|
}
|
|
103242
103370
|
const { toolCallId, sessionId, agentId } = JSON.parse(btnAction.value);
|
|
103243
|
-
clearApprovalTimeout2(toolCallId);
|
|
103371
|
+
clearApprovalTimeout2(this.outboundState, toolCallId);
|
|
103244
103372
|
const opts = { from: `slack:${btnBody.user?.id ?? "unknown"}` };
|
|
103245
103373
|
await relay.publish(`relay.system.approval.${agentId}`, {
|
|
103246
103374
|
type: "approval_response",
|
|
@@ -103353,9 +103481,13 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
|
|
|
103353
103481
|
if (!agentId) {
|
|
103354
103482
|
return { success: false, error: `Could not extract agentId from subject: ${subject}`, durationMs: Date.now() - startTime };
|
|
103355
103483
|
}
|
|
103484
|
+
const log = deps.logger ?? console;
|
|
103485
|
+
if (!deps.agentSessionStore) {
|
|
103486
|
+
log.warn("[CCA] agentSessionStore not provided \u2014 SDK session mapping will not persist across restarts");
|
|
103487
|
+
}
|
|
103356
103488
|
const persistedSdkSessionId = deps.agentSessionStore?.get(agentId);
|
|
103357
103489
|
const ccaSessionKey = persistedSdkSessionId ?? agentId;
|
|
103358
|
-
|
|
103490
|
+
log.debug?.(`[CCA] session lookup: agentId=${agentId}, persistedSdkSessionId=${persistedSdkSessionId ?? "(none)"}, hasStarted=${!!persistedSdkSessionId}`);
|
|
103359
103491
|
deps.traceStore.insertSpan({
|
|
103360
103492
|
messageId: envelope.id,
|
|
103361
103493
|
traceId: randomUUID5(),
|
|
@@ -103372,11 +103504,15 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
|
|
|
103372
103504
|
processedAt: null,
|
|
103373
103505
|
error: null
|
|
103374
103506
|
});
|
|
103375
|
-
const
|
|
103507
|
+
const payloadObj = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload : null;
|
|
103508
|
+
const bindingPerms = payloadObj?.__bindingPermissions;
|
|
103509
|
+
const responseContext = payloadObj?.responseContext;
|
|
103510
|
+
const payloadCwd = payloadObj?.cwd;
|
|
103376
103511
|
const effectiveCwd = payloadCwd ?? context?.agent?.directory;
|
|
103377
|
-
|
|
103512
|
+
const effectivePermissionMode = bindingPerms?.permissionMode ?? "default";
|
|
103513
|
+
log.debug?.(`[CCA] handleAgentMessage agentId=${agentId} ccaSessionKey=${ccaSessionKey}, payloadCwd=${payloadCwd ?? "(none)"}, context.agent.directory=${context?.agent?.directory ?? "(none)"}, resolvedCwd=${effectiveCwd ?? "(deferred to session)"}, permissionMode=${effectivePermissionMode}`);
|
|
103378
103514
|
deps.agentManager.ensureSession(ccaSessionKey, {
|
|
103379
|
-
permissionMode:
|
|
103515
|
+
permissionMode: effectivePermissionMode,
|
|
103380
103516
|
hasStarted: !!persistedSdkSessionId,
|
|
103381
103517
|
...effectiveCwd ? { cwd: effectiveCwd } : {}
|
|
103382
103518
|
});
|
|
@@ -103384,7 +103520,6 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
|
|
|
103384
103520
|
if (!envelope.replyTo) {
|
|
103385
103521
|
log.warn(`ClaudeCodeAdapter: envelope ${envelope.id} has no replyTo \u2014 response events will not be published`);
|
|
103386
103522
|
}
|
|
103387
|
-
const payloadObj = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload : null;
|
|
103388
103523
|
if (payloadObj?.type && STREAM_EVENT_TYPES.has(payloadObj.type)) {
|
|
103389
103524
|
log.debug?.(`[CCA] skipping sendMessage for StreamEvent payload type=${String(payloadObj.type)}`);
|
|
103390
103525
|
deps.traceStore.updateSpan(envelope.id, { status: "processed", processedAt: Date.now() });
|
|
@@ -103392,12 +103527,15 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
|
|
|
103392
103527
|
}
|
|
103393
103528
|
const correlationId = payloadObj?.correlationId;
|
|
103394
103529
|
const prompt2 = formatPromptWithContext(extractPayloadContent(envelope.payload), envelope, agentId, ccaSessionKey);
|
|
103530
|
+
const formatBlock = buildResponseFormatBlock(responseContext);
|
|
103395
103531
|
const ttlRemaining = envelope.budget.ttl - Date.now();
|
|
103396
103532
|
const controller = new AbortController();
|
|
103397
103533
|
const timeout = setTimeout(() => controller.abort(), ttlRemaining > 0 ? ttlRemaining : config.defaultTimeoutMs);
|
|
103398
103534
|
const isInboxReplyTo = envelope.replyTo?.startsWith("relay.inbox.");
|
|
103399
103535
|
const eventStream = deps.agentManager.sendMessage(ccaSessionKey, prompt2, {
|
|
103400
|
-
|
|
103536
|
+
permissionMode: effectivePermissionMode,
|
|
103537
|
+
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
103538
|
+
...formatBlock ? { systemPromptAppend: formatBlock } : {}
|
|
103401
103539
|
});
|
|
103402
103540
|
let eventCount = 0, collectedText = "", stepCounter = 0, messageBuffer = "";
|
|
103403
103541
|
let streamedDone = false, streamError;
|
|
@@ -103455,7 +103593,7 @@ async function handleAgentMessage(subject, envelope, context, startTime, config,
|
|
|
103455
103593
|
const actualSdkId = deps.agentManager.getSdkSessionId(ccaSessionKey);
|
|
103456
103594
|
if (actualSdkId && actualSdkId !== agentId) {
|
|
103457
103595
|
deps.agentSessionStore.set(agentId, actualSdkId);
|
|
103458
|
-
log.
|
|
103596
|
+
log.info(`[CCA] persisted session mapping: ${agentId} \u2192 ${actualSdkId}`);
|
|
103459
103597
|
} else {
|
|
103460
103598
|
log.debug?.(`[CCA] no session mapping to persist: agentId=${agentId}, ccaSessionKey=${ccaSessionKey}, actualSdkId=${actualSdkId ?? "(none)"}`);
|
|
103461
103599
|
}
|
|
@@ -103483,22 +103621,23 @@ function extractAgentId(subject) {
|
|
|
103483
103621
|
return null;
|
|
103484
103622
|
return segments[2] || null;
|
|
103485
103623
|
}
|
|
103486
|
-
|
|
103487
|
-
|
|
103488
|
-
|
|
103489
|
-
|
|
103490
|
-
|
|
103491
|
-
|
|
103492
|
-
|
|
103493
|
-
|
|
103494
|
-
|
|
103495
|
-
|
|
103496
|
-
"
|
|
103497
|
-
"
|
|
103498
|
-
|
|
103499
|
-
|
|
103500
|
-
|
|
103501
|
-
|
|
103624
|
+
function buildResponseFormatBlock(ctx) {
|
|
103625
|
+
if (!ctx?.platform)
|
|
103626
|
+
return "";
|
|
103627
|
+
const lines = [
|
|
103628
|
+
`Platform: ${ctx.platform}`,
|
|
103629
|
+
ctx.maxLength ? `Maximum response length: ${ctx.maxLength} characters` : ""
|
|
103630
|
+
];
|
|
103631
|
+
if (ctx.formattingInstructions) {
|
|
103632
|
+
lines.push("", ctx.formattingInstructions);
|
|
103633
|
+
} else if (ctx.supportedFormats && !ctx.supportedFormats.includes("markdown")) {
|
|
103634
|
+
lines.push("", "FORMATTING RULES (you MUST follow these):");
|
|
103635
|
+
lines.push("- Avoid complex Markdown formatting (tables, headings) \u2014 use plain text with bullet points.");
|
|
103636
|
+
}
|
|
103637
|
+
return `<response_format>
|
|
103638
|
+
${lines.filter(Boolean).join("\n")}
|
|
103639
|
+
</response_format>`;
|
|
103640
|
+
}
|
|
103502
103641
|
function formatPromptWithContext(content3, envelope, agentId, sdkSessionId) {
|
|
103503
103642
|
const lines = [
|
|
103504
103643
|
`Agent-ID: ${agentId}`,
|
|
@@ -103516,12 +103655,6 @@ function formatPromptWithContext(content3, envelope, agentId, sdkSessionId) {
|
|
|
103516
103655
|
if (envelope.replyTo) {
|
|
103517
103656
|
lines.push("", `Reply to: ${envelope.replyTo}`, "If you cannot complete the task within the budget, summarize what you've done and stop.");
|
|
103518
103657
|
}
|
|
103519
|
-
const payloadObj = typeof envelope.payload === "object" && envelope.payload !== null ? envelope.payload : null;
|
|
103520
|
-
const responseContext = payloadObj?.responseContext;
|
|
103521
|
-
const platform2 = responseContext?.platform;
|
|
103522
|
-
if (platform2 && PLATFORM_FORMATTING[platform2]) {
|
|
103523
|
-
lines.push("", PLATFORM_FORMATTING[platform2]);
|
|
103524
|
-
}
|
|
103525
103658
|
return `<relay_context>
|
|
103526
103659
|
${lines.join("\n")}
|
|
103527
103660
|
</relay_context>
|
|
@@ -104066,11 +104199,293 @@ import { z as z24 } from "zod";
|
|
|
104066
104199
|
// ../../apps/server/src/services/relay/adapter-manager.ts
|
|
104067
104200
|
import { readFile as readFile8 } from "node:fs/promises";
|
|
104068
104201
|
import { createRequire } from "node:module";
|
|
104069
|
-
import { dirname as
|
|
104202
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
104203
|
+
|
|
104204
|
+
// ../../apps/server/src/services/relay/adapter-error.ts
|
|
104205
|
+
var AdapterError = class extends Error {
|
|
104206
|
+
constructor(message, code3) {
|
|
104207
|
+
super(message);
|
|
104208
|
+
this.code = code3;
|
|
104209
|
+
this.name = "AdapterError";
|
|
104210
|
+
}
|
|
104211
|
+
};
|
|
104212
|
+
|
|
104213
|
+
// ../../apps/server/src/services/relay/adapter-config.ts
|
|
104214
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, rename as rename2 } from "node:fs/promises";
|
|
104215
|
+
import { dirname as dirname3 } from "node:path";
|
|
104216
|
+
var CONFIG_STABILITY_THRESHOLD_MS = 150;
|
|
104217
|
+
var CONFIG_POLL_INTERVAL_MS = 50;
|
|
104218
|
+
async function loadAdapterConfig(configPath) {
|
|
104219
|
+
try {
|
|
104220
|
+
const raw = await readFile4(configPath, "utf-8");
|
|
104221
|
+
const parsed = AdaptersConfigFileSchema.safeParse(JSON.parse(raw));
|
|
104222
|
+
if (parsed.success) {
|
|
104223
|
+
return parsed.data.adapters;
|
|
104224
|
+
} else {
|
|
104225
|
+
logger.warn(
|
|
104226
|
+
"[AdapterConfig] Malformed config, skipping invalid entries:",
|
|
104227
|
+
parsed.error.flatten()
|
|
104228
|
+
);
|
|
104229
|
+
return [];
|
|
104230
|
+
}
|
|
104231
|
+
} catch (err) {
|
|
104232
|
+
if (err.code === "ENOENT") {
|
|
104233
|
+
return [];
|
|
104234
|
+
} else {
|
|
104235
|
+
logger.warn("[AdapterConfig] Failed to read config:", err);
|
|
104236
|
+
return [];
|
|
104237
|
+
}
|
|
104238
|
+
}
|
|
104239
|
+
}
|
|
104240
|
+
async function saveAdapterConfig(configPath, configs) {
|
|
104241
|
+
await mkdir4(dirname3(configPath), { recursive: true });
|
|
104242
|
+
const tmpPath = `${configPath}.tmp`;
|
|
104243
|
+
await writeFile2(
|
|
104244
|
+
tmpPath,
|
|
104245
|
+
JSON.stringify({ adapters: configs }, null, 2),
|
|
104246
|
+
"utf-8"
|
|
104247
|
+
);
|
|
104248
|
+
await rename2(tmpPath, configPath);
|
|
104249
|
+
}
|
|
104250
|
+
async function ensureDefaultAdapterConfig(configPath) {
|
|
104251
|
+
try {
|
|
104252
|
+
await readFile4(configPath, "utf-8");
|
|
104253
|
+
} catch (err) {
|
|
104254
|
+
if (err.code === "ENOENT") {
|
|
104255
|
+
const defaultConfig = {
|
|
104256
|
+
adapters: [
|
|
104257
|
+
{
|
|
104258
|
+
id: "claude-code",
|
|
104259
|
+
type: "claude-code",
|
|
104260
|
+
builtin: true,
|
|
104261
|
+
enabled: true,
|
|
104262
|
+
config: {
|
|
104263
|
+
maxConcurrent: 3,
|
|
104264
|
+
defaultTimeoutMs: 3e5
|
|
104265
|
+
}
|
|
104266
|
+
}
|
|
104267
|
+
]
|
|
104268
|
+
};
|
|
104269
|
+
try {
|
|
104270
|
+
await mkdir4(dirname3(configPath), { recursive: true });
|
|
104271
|
+
await writeFile2(
|
|
104272
|
+
configPath,
|
|
104273
|
+
JSON.stringify(defaultConfig, null, 2),
|
|
104274
|
+
"utf-8"
|
|
104275
|
+
);
|
|
104276
|
+
logger.info("[AdapterConfig] Generated default adapters.json with claude-code adapter");
|
|
104277
|
+
} catch (writeErr) {
|
|
104278
|
+
logger.warn("[AdapterConfig] Failed to write default config:", writeErr);
|
|
104279
|
+
}
|
|
104280
|
+
}
|
|
104281
|
+
}
|
|
104282
|
+
}
|
|
104283
|
+
function watchAdapterConfig(configPath, onChange) {
|
|
104284
|
+
const watcher = esm_default.watch(configPath, {
|
|
104285
|
+
persistent: true,
|
|
104286
|
+
ignoreInitial: true,
|
|
104287
|
+
awaitWriteFinish: {
|
|
104288
|
+
stabilityThreshold: CONFIG_STABILITY_THRESHOLD_MS,
|
|
104289
|
+
pollInterval: CONFIG_POLL_INTERVAL_MS
|
|
104290
|
+
}
|
|
104291
|
+
});
|
|
104292
|
+
watcher.on("change", onChange);
|
|
104293
|
+
return watcher;
|
|
104294
|
+
}
|
|
104295
|
+
function maskSensitiveFields(config, manifest) {
|
|
104296
|
+
if (!manifest) return config;
|
|
104297
|
+
const masked = structuredClone(config);
|
|
104298
|
+
for (const field of manifest.configFields) {
|
|
104299
|
+
if (field.type !== "password") continue;
|
|
104300
|
+
const parts = field.key.split(".");
|
|
104301
|
+
let current = masked;
|
|
104302
|
+
let found = true;
|
|
104303
|
+
for (let i2 = 0; i2 < parts.length - 1; i2++) {
|
|
104304
|
+
if (current[parts[i2]] && typeof current[parts[i2]] === "object") {
|
|
104305
|
+
current = current[parts[i2]];
|
|
104306
|
+
} else {
|
|
104307
|
+
found = false;
|
|
104308
|
+
break;
|
|
104309
|
+
}
|
|
104310
|
+
}
|
|
104311
|
+
const lastKey = parts.at(-1);
|
|
104312
|
+
if (found && lastKey in current) {
|
|
104313
|
+
current[lastKey] = "***";
|
|
104314
|
+
}
|
|
104315
|
+
}
|
|
104316
|
+
return masked;
|
|
104317
|
+
}
|
|
104318
|
+
function mergeWithPasswordPreservation(existing, incoming, manifest) {
|
|
104319
|
+
const result2 = { ...existing, ...incoming };
|
|
104320
|
+
if (!manifest) return result2;
|
|
104321
|
+
for (const field of manifest.configFields) {
|
|
104322
|
+
if (field.type !== "password") continue;
|
|
104323
|
+
const parts = field.key.split(".");
|
|
104324
|
+
const incomingValue = getNestedValue(incoming, parts);
|
|
104325
|
+
if (incomingValue === "" || incomingValue === "***" || incomingValue === void 0) {
|
|
104326
|
+
const existingValue = getNestedValue(existing, parts);
|
|
104327
|
+
if (existingValue !== void 0) {
|
|
104328
|
+
setNestedValue(result2, parts, existingValue);
|
|
104329
|
+
}
|
|
104330
|
+
}
|
|
104331
|
+
}
|
|
104332
|
+
return result2;
|
|
104333
|
+
}
|
|
104334
|
+
function getNestedValue(obj, parts) {
|
|
104335
|
+
let current = obj;
|
|
104336
|
+
for (const part of parts) {
|
|
104337
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
104338
|
+
current = current[part];
|
|
104339
|
+
}
|
|
104340
|
+
return current;
|
|
104341
|
+
}
|
|
104342
|
+
function setNestedValue(obj, parts, value) {
|
|
104343
|
+
let current = obj;
|
|
104344
|
+
for (let i2 = 0; i2 < parts.length - 1; i2++) {
|
|
104345
|
+
if (!(parts[i2] in current) || typeof current[parts[i2]] !== "object") {
|
|
104346
|
+
current[parts[i2]] = {};
|
|
104347
|
+
}
|
|
104348
|
+
current = current[parts[i2]];
|
|
104349
|
+
}
|
|
104350
|
+
current[parts.at(-1)] = value;
|
|
104351
|
+
}
|
|
104352
|
+
|
|
104353
|
+
// ../../apps/server/src/services/relay/adapter-factory.ts
|
|
104354
|
+
import { dirname as dirname4 } from "node:path";
|
|
104355
|
+
function defaultAdapterStatus() {
|
|
104356
|
+
return {
|
|
104357
|
+
state: "disconnected",
|
|
104358
|
+
messageCount: { inbound: 0, outbound: 0 },
|
|
104359
|
+
errorCount: 0
|
|
104360
|
+
};
|
|
104361
|
+
}
|
|
104362
|
+
async function createAdapter(config, deps, configPath, onPluginManifest) {
|
|
104363
|
+
switch (config.type) {
|
|
104364
|
+
case "telegram": {
|
|
104365
|
+
const adapter = new TelegramAdapter(
|
|
104366
|
+
config.id,
|
|
104367
|
+
config.config
|
|
104368
|
+
);
|
|
104369
|
+
adapter.setLogger(createTaggedLogger(`telegram:${config.id}`));
|
|
104370
|
+
return adapter;
|
|
104371
|
+
}
|
|
104372
|
+
case "webhook":
|
|
104373
|
+
return new WebhookAdapter(
|
|
104374
|
+
config.id,
|
|
104375
|
+
config.config
|
|
104376
|
+
);
|
|
104377
|
+
case "slack": {
|
|
104378
|
+
const adapter = new SlackAdapter(
|
|
104379
|
+
config.id,
|
|
104380
|
+
config.config
|
|
104381
|
+
);
|
|
104382
|
+
adapter.setLogger(createTaggedLogger(`slack:${config.id}`));
|
|
104383
|
+
return adapter;
|
|
104384
|
+
}
|
|
104385
|
+
case "claude-code":
|
|
104386
|
+
return new ClaudeCodeAdapter(
|
|
104387
|
+
config.id,
|
|
104388
|
+
config.config,
|
|
104389
|
+
{
|
|
104390
|
+
agentManager: deps.agentManager,
|
|
104391
|
+
traceStore: deps.traceStore,
|
|
104392
|
+
pulseStore: deps.pulseStore,
|
|
104393
|
+
agentSessionStore: deps.agentSessionStore,
|
|
104394
|
+
logger
|
|
104395
|
+
}
|
|
104396
|
+
);
|
|
104397
|
+
case "plugin":
|
|
104398
|
+
return loadPluginAdapter(config, configPath, onPluginManifest);
|
|
104399
|
+
default:
|
|
104400
|
+
logger.warn(`[AdapterFactory] Unknown adapter type: ${config.type}`);
|
|
104401
|
+
return null;
|
|
104402
|
+
}
|
|
104403
|
+
}
|
|
104404
|
+
var CONNECTION_TEST_TIMEOUT_MS = 15e3;
|
|
104405
|
+
async function testAdapterConnection(adapter) {
|
|
104406
|
+
try {
|
|
104407
|
+
if (adapter.testConnection) {
|
|
104408
|
+
let timer;
|
|
104409
|
+
try {
|
|
104410
|
+
return await Promise.race([
|
|
104411
|
+
adapter.testConnection(),
|
|
104412
|
+
new Promise((_4, reject) => {
|
|
104413
|
+
timer = setTimeout(
|
|
104414
|
+
() => reject(new Error("Connection test timed out")),
|
|
104415
|
+
CONNECTION_TEST_TIMEOUT_MS
|
|
104416
|
+
);
|
|
104417
|
+
})
|
|
104418
|
+
]);
|
|
104419
|
+
} finally {
|
|
104420
|
+
clearTimeout(timer);
|
|
104421
|
+
}
|
|
104422
|
+
}
|
|
104423
|
+
const noopRelay = {
|
|
104424
|
+
publish: async () => ({ messageId: "", deliveredTo: 0 }),
|
|
104425
|
+
onSignal: () => () => {
|
|
104426
|
+
},
|
|
104427
|
+
subscribe: () => () => {
|
|
104428
|
+
}
|
|
104429
|
+
};
|
|
104430
|
+
let fallbackTimer;
|
|
104431
|
+
try {
|
|
104432
|
+
await Promise.race([
|
|
104433
|
+
adapter.start(noopRelay),
|
|
104434
|
+
new Promise((_4, reject) => {
|
|
104435
|
+
fallbackTimer = setTimeout(
|
|
104436
|
+
() => reject(new Error("Connection test timed out")),
|
|
104437
|
+
CONNECTION_TEST_TIMEOUT_MS
|
|
104438
|
+
);
|
|
104439
|
+
})
|
|
104440
|
+
]);
|
|
104441
|
+
} finally {
|
|
104442
|
+
clearTimeout(fallbackTimer);
|
|
104443
|
+
}
|
|
104444
|
+
return { ok: true };
|
|
104445
|
+
} catch (err) {
|
|
104446
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104447
|
+
return { ok: false, error: message };
|
|
104448
|
+
} finally {
|
|
104449
|
+
try {
|
|
104450
|
+
await adapter.stop();
|
|
104451
|
+
} catch {
|
|
104452
|
+
}
|
|
104453
|
+
}
|
|
104454
|
+
}
|
|
104455
|
+
async function loadPluginAdapter(config, configPath, onPluginManifest) {
|
|
104456
|
+
if (!config.plugin) {
|
|
104457
|
+
logger.warn(`[AdapterFactory] Plugin adapter '${config.id}' missing plugin source config`);
|
|
104458
|
+
return null;
|
|
104459
|
+
}
|
|
104460
|
+
const builtinMap = /* @__PURE__ */ new Map();
|
|
104461
|
+
const configDir = dirname4(configPath);
|
|
104462
|
+
const results = await loadAdapters(
|
|
104463
|
+
[
|
|
104464
|
+
{
|
|
104465
|
+
id: config.id,
|
|
104466
|
+
type: config.type,
|
|
104467
|
+
enabled: config.enabled,
|
|
104468
|
+
plugin: config.plugin,
|
|
104469
|
+
config: config.config
|
|
104470
|
+
}
|
|
104471
|
+
],
|
|
104472
|
+
builtinMap,
|
|
104473
|
+
configDir
|
|
104474
|
+
);
|
|
104475
|
+
const result2 = results[0];
|
|
104476
|
+
if (!result2) return null;
|
|
104477
|
+
if (result2.manifest && onPluginManifest) {
|
|
104478
|
+
onPluginManifest(config.type, result2.manifest);
|
|
104479
|
+
}
|
|
104480
|
+
return result2.adapter;
|
|
104481
|
+
}
|
|
104482
|
+
|
|
104483
|
+
// ../../apps/server/src/services/relay/binding-subsystem.ts
|
|
104484
|
+
import { dirname as dirname6 } from "node:path";
|
|
104070
104485
|
|
|
104071
104486
|
// ../../apps/server/src/services/relay/binding-store.ts
|
|
104072
|
-
import { readFile as
|
|
104073
|
-
import { dirname as
|
|
104487
|
+
import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir5, rename as rename3, stat as stat4 } from "node:fs/promises";
|
|
104488
|
+
import { dirname as dirname5, join as pathJoin } from "node:path";
|
|
104074
104489
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
104075
104490
|
import { z as z23 } from "zod";
|
|
104076
104491
|
var BindingsFileSchema = z23.object({
|
|
@@ -104204,7 +104619,7 @@ var BindingStore = class {
|
|
|
104204
104619
|
}
|
|
104205
104620
|
async load() {
|
|
104206
104621
|
try {
|
|
104207
|
-
const raw = await
|
|
104622
|
+
const raw = await readFile5(this.filePath, "utf-8");
|
|
104208
104623
|
const json = JSON.parse(raw);
|
|
104209
104624
|
const parsed = BindingsFileSchema.parse(json);
|
|
104210
104625
|
this.bindings.clear();
|
|
@@ -104224,10 +104639,10 @@ var BindingStore = class {
|
|
|
104224
104639
|
}
|
|
104225
104640
|
async save() {
|
|
104226
104641
|
const data = { bindings: this.getAll() };
|
|
104227
|
-
await
|
|
104642
|
+
await mkdir5(dirname5(this.filePath), { recursive: true });
|
|
104228
104643
|
const tmpPath = `${this.filePath}.tmp`;
|
|
104229
|
-
await
|
|
104230
|
-
await
|
|
104644
|
+
await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
104645
|
+
await rename3(tmpPath, this.filePath);
|
|
104231
104646
|
const fileStat = await stat4(this.filePath);
|
|
104232
104647
|
this.lastWriteMtime = fileStat.mtimeMs;
|
|
104233
104648
|
}
|
|
@@ -104257,10 +104672,12 @@ var BindingStore = class {
|
|
|
104257
104672
|
|
|
104258
104673
|
// ../../apps/server/src/services/relay/agent-session-store.ts
|
|
104259
104674
|
import { join as join11 } from "node:path";
|
|
104260
|
-
import { readFile as
|
|
104675
|
+
import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir6, rename as rename4 } from "node:fs/promises";
|
|
104261
104676
|
var AgentSessionStore = class {
|
|
104262
104677
|
filePath;
|
|
104263
104678
|
sessions = /* @__PURE__ */ new Map();
|
|
104679
|
+
/** Serializes persist calls to prevent concurrent tmp+rename races. */
|
|
104680
|
+
writeLock = Promise.resolve();
|
|
104264
104681
|
constructor(relayDir) {
|
|
104265
104682
|
this.filePath = join11(relayDir, "agent-sessions.json");
|
|
104266
104683
|
}
|
|
@@ -104272,7 +104689,7 @@ var AgentSessionStore = class {
|
|
|
104272
104689
|
*/
|
|
104273
104690
|
async init() {
|
|
104274
104691
|
try {
|
|
104275
|
-
const raw = await
|
|
104692
|
+
const raw = await readFile6(this.filePath, "utf-8");
|
|
104276
104693
|
const parsed = JSON.parse(raw);
|
|
104277
104694
|
this.sessions = new Map(Object.entries(parsed));
|
|
104278
104695
|
logger.info(`[AgentSessionStore] Loaded ${this.sessions.size} session mapping(s)`);
|
|
@@ -104328,24 +104745,41 @@ var AgentSessionStore = class {
|
|
|
104328
104745
|
logger.warn("[AgentSessionStore] Failed to persist after delete", { err });
|
|
104329
104746
|
});
|
|
104330
104747
|
}
|
|
104331
|
-
/**
|
|
104332
|
-
|
|
104748
|
+
/**
|
|
104749
|
+
* Flush any pending writes to disk.
|
|
104750
|
+
*
|
|
104751
|
+
* Call during shutdown to ensure the last fire-and-forget persist
|
|
104752
|
+
* completes before the process exits.
|
|
104753
|
+
*/
|
|
104754
|
+
async shutdown() {
|
|
104755
|
+
try {
|
|
104756
|
+
await this.writeLock;
|
|
104757
|
+
} catch {
|
|
104758
|
+
}
|
|
104759
|
+
}
|
|
104760
|
+
/** Enqueue an atomic persist, serialized to prevent concurrent tmp+rename races. */
|
|
104761
|
+
persist() {
|
|
104762
|
+
this.writeLock = this.writeLock.then(() => this.doPersist(), () => this.doPersist());
|
|
104763
|
+
return this.writeLock;
|
|
104764
|
+
}
|
|
104765
|
+
/** Atomic tmp+rename write. Must be serialized via writeLock. */
|
|
104766
|
+
async doPersist() {
|
|
104333
104767
|
const dir = join11(this.filePath, "..");
|
|
104334
|
-
await
|
|
104768
|
+
await mkdir6(dir, { recursive: true });
|
|
104335
104769
|
const data = {};
|
|
104336
104770
|
for (const [agentId, record2] of this.sessions) {
|
|
104337
104771
|
data[agentId] = record2;
|
|
104338
104772
|
}
|
|
104339
104773
|
const json = JSON.stringify(data, null, 2);
|
|
104340
104774
|
const tmp = `${this.filePath}.tmp`;
|
|
104341
|
-
await
|
|
104342
|
-
await
|
|
104775
|
+
await writeFile4(tmp, json, "utf-8");
|
|
104776
|
+
await rename4(tmp, this.filePath);
|
|
104343
104777
|
}
|
|
104344
104778
|
};
|
|
104345
104779
|
|
|
104346
104780
|
// ../../apps/server/src/services/relay/binding-router.ts
|
|
104347
104781
|
import { join as pathJoin2 } from "node:path";
|
|
104348
|
-
import { readFile as
|
|
104782
|
+
import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir7, rename as rename5 } from "node:fs/promises";
|
|
104349
104783
|
var BindingRouter = class _BindingRouter {
|
|
104350
104784
|
constructor(deps) {
|
|
104351
104785
|
this.deps = deps;
|
|
@@ -104359,6 +104793,10 @@ var BindingRouter = class _BindingRouter {
|
|
|
104359
104793
|
inFlight = /* @__PURE__ */ new Map();
|
|
104360
104794
|
sessionMapPath;
|
|
104361
104795
|
unsubscribe;
|
|
104796
|
+
/** Serializes saveSessionMap calls to prevent concurrent tmp+rename races. */
|
|
104797
|
+
writeLock = Promise.resolve();
|
|
104798
|
+
/** Guards against concurrent shutdown calls corrupting session data. */
|
|
104799
|
+
isShutdown = false;
|
|
104362
104800
|
/** Load persisted session map, subscribe to inbound messages. */
|
|
104363
104801
|
async init() {
|
|
104364
104802
|
await this.loadSessionMap();
|
|
@@ -104555,7 +104993,7 @@ var BindingRouter = class _BindingRouter {
|
|
|
104555
104993
|
}
|
|
104556
104994
|
async loadSessionMap() {
|
|
104557
104995
|
try {
|
|
104558
|
-
const raw = await
|
|
104996
|
+
const raw = await readFile7(this.sessionMapPath, "utf-8");
|
|
104559
104997
|
const parsed = JSON.parse(raw);
|
|
104560
104998
|
if (!Array.isArray(parsed)) {
|
|
104561
104999
|
logger.warn("BindingRouter: sessionMap is not an array, starting fresh");
|
|
@@ -104571,19 +105009,32 @@ var BindingRouter = class _BindingRouter {
|
|
|
104571
105009
|
);
|
|
104572
105010
|
}
|
|
104573
105011
|
this.sessionMap = new Map(valid);
|
|
104574
|
-
} catch {
|
|
105012
|
+
} catch (err) {
|
|
105013
|
+
const code3 = err.code;
|
|
105014
|
+
if (code3 === "ENOENT") {
|
|
105015
|
+
logger.debug("BindingRouter: no sessions.json found, starting with empty session map");
|
|
105016
|
+
} else {
|
|
105017
|
+
logger.warn("BindingRouter: failed to load sessions.json, starting fresh", err);
|
|
105018
|
+
}
|
|
104575
105019
|
this.sessionMap = /* @__PURE__ */ new Map();
|
|
104576
105020
|
}
|
|
104577
105021
|
}
|
|
104578
|
-
|
|
104579
|
-
|
|
105022
|
+
saveSessionMap() {
|
|
105023
|
+
this.writeLock = this.writeLock.then(() => this.doSaveSessionMap(), () => this.doSaveSessionMap());
|
|
105024
|
+
return this.writeLock;
|
|
105025
|
+
}
|
|
105026
|
+
/** Atomic tmp+rename write of the session map. Must be serialized via writeLock. */
|
|
105027
|
+
async doSaveSessionMap() {
|
|
105028
|
+
await mkdir7(this.deps.relayDir, { recursive: true });
|
|
104580
105029
|
const data = JSON.stringify(Array.from(this.sessionMap.entries()));
|
|
104581
105030
|
const tmpPath = `${this.sessionMapPath}.tmp`;
|
|
104582
|
-
await
|
|
104583
|
-
await
|
|
105031
|
+
await writeFile5(tmpPath, data, "utf-8");
|
|
105032
|
+
await rename5(tmpPath, this.sessionMapPath);
|
|
104584
105033
|
}
|
|
104585
|
-
/** Save session map, unsubscribe, and clear state. */
|
|
105034
|
+
/** Save session map, unsubscribe, and clear state. Idempotent — safe to call multiple times. */
|
|
104586
105035
|
async shutdown() {
|
|
105036
|
+
if (this.isShutdown) return;
|
|
105037
|
+
this.isShutdown = true;
|
|
104587
105038
|
this.unsubscribe?.();
|
|
104588
105039
|
try {
|
|
104589
105040
|
await this.saveSessionMap();
|
|
@@ -104594,284 +105045,82 @@ var BindingRouter = class _BindingRouter {
|
|
|
104594
105045
|
}
|
|
104595
105046
|
};
|
|
104596
105047
|
|
|
104597
|
-
// ../../apps/server/src/services/relay/
|
|
104598
|
-
var
|
|
104599
|
-
|
|
104600
|
-
|
|
104601
|
-
|
|
104602
|
-
|
|
104603
|
-
|
|
104604
|
-
|
|
104605
|
-
|
|
104606
|
-
// ../../apps/server/src/services/relay/adapter-config.ts
|
|
104607
|
-
import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir7, rename as rename5 } from "node:fs/promises";
|
|
104608
|
-
import { dirname as dirname4 } from "node:path";
|
|
104609
|
-
var CONFIG_STABILITY_THRESHOLD_MS = 150;
|
|
104610
|
-
var CONFIG_POLL_INTERVAL_MS = 50;
|
|
104611
|
-
async function loadAdapterConfig(configPath) {
|
|
104612
|
-
try {
|
|
104613
|
-
const raw = await readFile7(configPath, "utf-8");
|
|
104614
|
-
const parsed = AdaptersConfigFileSchema.safeParse(JSON.parse(raw));
|
|
104615
|
-
if (parsed.success) {
|
|
104616
|
-
return parsed.data.adapters;
|
|
104617
|
-
} else {
|
|
104618
|
-
logger.warn(
|
|
104619
|
-
"[AdapterConfig] Malformed config, skipping invalid entries:",
|
|
104620
|
-
parsed.error.flatten()
|
|
104621
|
-
);
|
|
104622
|
-
return [];
|
|
104623
|
-
}
|
|
104624
|
-
} catch (err) {
|
|
104625
|
-
if (err.code === "ENOENT") {
|
|
104626
|
-
return [];
|
|
104627
|
-
} else {
|
|
104628
|
-
logger.warn("[AdapterConfig] Failed to read config:", err);
|
|
104629
|
-
return [];
|
|
104630
|
-
}
|
|
105048
|
+
// ../../apps/server/src/services/relay/binding-subsystem.ts
|
|
105049
|
+
var BindingSubsystem = class _BindingSubsystem {
|
|
105050
|
+
bindingStore;
|
|
105051
|
+
agentSessionStore;
|
|
105052
|
+
bindingRouter;
|
|
105053
|
+
isShutdown = false;
|
|
105054
|
+
constructor(bindingStore, agentSessionStore) {
|
|
105055
|
+
this.bindingStore = bindingStore;
|
|
105056
|
+
this.agentSessionStore = agentSessionStore;
|
|
104631
105057
|
}
|
|
104632
|
-
|
|
104633
|
-
|
|
104634
|
-
|
|
104635
|
-
|
|
104636
|
-
|
|
104637
|
-
|
|
104638
|
-
|
|
104639
|
-
|
|
104640
|
-
|
|
104641
|
-
|
|
104642
|
-
|
|
104643
|
-
|
|
104644
|
-
|
|
104645
|
-
|
|
104646
|
-
|
|
104647
|
-
|
|
104648
|
-
|
|
104649
|
-
|
|
104650
|
-
|
|
104651
|
-
|
|
104652
|
-
|
|
104653
|
-
|
|
104654
|
-
|
|
104655
|
-
|
|
104656
|
-
|
|
104657
|
-
|
|
104658
|
-
}
|
|
104659
|
-
}
|
|
104660
|
-
]
|
|
105058
|
+
/**
|
|
105059
|
+
* Initialize the binding subsystem: BindingStore, AgentSessionStore, and BindingRouter.
|
|
105060
|
+
*
|
|
105061
|
+
* Non-fatal — if initialization fails, returns undefined and logs a warning.
|
|
105062
|
+
* AdapterManager continues running without binding-based routing.
|
|
105063
|
+
*
|
|
105064
|
+
* @param deps - Required dependencies for subsystem initialization
|
|
105065
|
+
* @returns Initialized subsystem, or undefined on failure
|
|
105066
|
+
*/
|
|
105067
|
+
static async init(deps) {
|
|
105068
|
+
const relayDir = dirname6(deps.configPath);
|
|
105069
|
+
try {
|
|
105070
|
+
const bindingStore = new BindingStore(relayDir);
|
|
105071
|
+
await bindingStore.init();
|
|
105072
|
+
logger.info("[BindingSubsystem] BindingStore initialized");
|
|
105073
|
+
const agentSessionStore = new AgentSessionStore(relayDir);
|
|
105074
|
+
await agentSessionStore.init();
|
|
105075
|
+
logger.info("[BindingSubsystem] AgentSessionStore initialized");
|
|
105076
|
+
const subsystem = new _BindingSubsystem(bindingStore, agentSessionStore);
|
|
105077
|
+
const agentManager = deps.agentManager;
|
|
105078
|
+
const sessionCreator = {
|
|
105079
|
+
async createSession(cwd, permissionMode) {
|
|
105080
|
+
const id = crypto.randomUUID();
|
|
105081
|
+
agentManager.ensureSession(id, { permissionMode: permissionMode ?? "acceptEdits", cwd });
|
|
105082
|
+
return { id };
|
|
105083
|
+
}
|
|
104661
105084
|
};
|
|
104662
|
-
|
|
104663
|
-
|
|
104664
|
-
|
|
104665
|
-
|
|
104666
|
-
|
|
104667
|
-
|
|
104668
|
-
|
|
104669
|
-
|
|
104670
|
-
|
|
104671
|
-
|
|
104672
|
-
|
|
104673
|
-
}
|
|
104674
|
-
|
|
104675
|
-
|
|
104676
|
-
function watchAdapterConfig(configPath, onChange) {
|
|
104677
|
-
const watcher = esm_default.watch(configPath, {
|
|
104678
|
-
persistent: true,
|
|
104679
|
-
ignoreInitial: true,
|
|
104680
|
-
awaitWriteFinish: {
|
|
104681
|
-
stabilityThreshold: CONFIG_STABILITY_THRESHOLD_MS,
|
|
104682
|
-
pollInterval: CONFIG_POLL_INTERVAL_MS
|
|
104683
|
-
}
|
|
104684
|
-
});
|
|
104685
|
-
watcher.on("change", onChange);
|
|
104686
|
-
return watcher;
|
|
104687
|
-
}
|
|
104688
|
-
function maskSensitiveFields(config, manifest) {
|
|
104689
|
-
if (!manifest) return config;
|
|
104690
|
-
const masked = structuredClone(config);
|
|
104691
|
-
for (const field of manifest.configFields) {
|
|
104692
|
-
if (field.type !== "password") continue;
|
|
104693
|
-
const parts = field.key.split(".");
|
|
104694
|
-
let current = masked;
|
|
104695
|
-
let found = true;
|
|
104696
|
-
for (let i2 = 0; i2 < parts.length - 1; i2++) {
|
|
104697
|
-
if (current[parts[i2]] && typeof current[parts[i2]] === "object") {
|
|
104698
|
-
current = current[parts[i2]];
|
|
104699
|
-
} else {
|
|
104700
|
-
found = false;
|
|
104701
|
-
break;
|
|
104702
|
-
}
|
|
104703
|
-
}
|
|
104704
|
-
const lastKey = parts.at(-1);
|
|
104705
|
-
if (found && lastKey in current) {
|
|
104706
|
-
current[lastKey] = "***";
|
|
104707
|
-
}
|
|
104708
|
-
}
|
|
104709
|
-
return masked;
|
|
104710
|
-
}
|
|
104711
|
-
function mergeWithPasswordPreservation(existing, incoming, manifest) {
|
|
104712
|
-
const result2 = { ...existing, ...incoming };
|
|
104713
|
-
if (!manifest) return result2;
|
|
104714
|
-
for (const field of manifest.configFields) {
|
|
104715
|
-
if (field.type !== "password") continue;
|
|
104716
|
-
const parts = field.key.split(".");
|
|
104717
|
-
const incomingValue = getNestedValue(incoming, parts);
|
|
104718
|
-
if (incomingValue === "" || incomingValue === "***" || incomingValue === void 0) {
|
|
104719
|
-
const existingValue = getNestedValue(existing, parts);
|
|
104720
|
-
if (existingValue !== void 0) {
|
|
104721
|
-
setNestedValue(result2, parts, existingValue);
|
|
104722
|
-
}
|
|
105085
|
+
subsystem.bindingRouter = new BindingRouter({
|
|
105086
|
+
bindingStore,
|
|
105087
|
+
relayCore: deps.relayCore,
|
|
105088
|
+
agentManager: sessionCreator,
|
|
105089
|
+
meshCore: deps.meshCore,
|
|
105090
|
+
relayDir,
|
|
105091
|
+
resolveAdapterInstanceId: deps.resolveAdapterInstanceId
|
|
105092
|
+
});
|
|
105093
|
+
await subsystem.bindingRouter.init();
|
|
105094
|
+
logger.info("[BindingSubsystem] BindingRouter initialized");
|
|
105095
|
+
return subsystem;
|
|
105096
|
+
} catch (err) {
|
|
105097
|
+
logger.warn("[BindingSubsystem] Failed to initialize binding subsystem:", err);
|
|
105098
|
+
return void 0;
|
|
104723
105099
|
}
|
|
104724
105100
|
}
|
|
104725
|
-
|
|
104726
|
-
|
|
104727
|
-
|
|
104728
|
-
let current = obj;
|
|
104729
|
-
for (const part of parts) {
|
|
104730
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
104731
|
-
current = current[part];
|
|
105101
|
+
/** Get the BindingStore. */
|
|
105102
|
+
getBindingStore() {
|
|
105103
|
+
return this.bindingStore;
|
|
104732
105104
|
}
|
|
104733
|
-
|
|
104734
|
-
|
|
104735
|
-
|
|
104736
|
-
let current = obj;
|
|
104737
|
-
for (let i2 = 0; i2 < parts.length - 1; i2++) {
|
|
104738
|
-
if (!(parts[i2] in current) || typeof current[parts[i2]] !== "object") {
|
|
104739
|
-
current[parts[i2]] = {};
|
|
104740
|
-
}
|
|
104741
|
-
current = current[parts[i2]];
|
|
105105
|
+
/** Get the AgentSessionStore. */
|
|
105106
|
+
getAgentSessionStore() {
|
|
105107
|
+
return this.agentSessionStore;
|
|
104742
105108
|
}
|
|
104743
|
-
|
|
104744
|
-
|
|
104745
|
-
|
|
104746
|
-
// ../../apps/server/src/services/relay/adapter-factory.ts
|
|
104747
|
-
import { dirname as dirname5 } from "node:path";
|
|
104748
|
-
function defaultAdapterStatus() {
|
|
104749
|
-
return {
|
|
104750
|
-
state: "disconnected",
|
|
104751
|
-
messageCount: { inbound: 0, outbound: 0 },
|
|
104752
|
-
errorCount: 0
|
|
104753
|
-
};
|
|
104754
|
-
}
|
|
104755
|
-
async function createAdapter(config, deps, configPath, onPluginManifest) {
|
|
104756
|
-
switch (config.type) {
|
|
104757
|
-
case "telegram": {
|
|
104758
|
-
const adapter = new TelegramAdapter(
|
|
104759
|
-
config.id,
|
|
104760
|
-
config.config
|
|
104761
|
-
);
|
|
104762
|
-
adapter.setLogger(createTaggedLogger(`telegram:${config.id}`));
|
|
104763
|
-
return adapter;
|
|
104764
|
-
}
|
|
104765
|
-
case "webhook":
|
|
104766
|
-
return new WebhookAdapter(
|
|
104767
|
-
config.id,
|
|
104768
|
-
config.config
|
|
104769
|
-
);
|
|
104770
|
-
case "slack": {
|
|
104771
|
-
const adapter = new SlackAdapter(
|
|
104772
|
-
config.id,
|
|
104773
|
-
config.config
|
|
104774
|
-
);
|
|
104775
|
-
adapter.setLogger(createTaggedLogger(`slack:${config.id}`));
|
|
104776
|
-
return adapter;
|
|
104777
|
-
}
|
|
104778
|
-
case "claude-code":
|
|
104779
|
-
return new ClaudeCodeAdapter(
|
|
104780
|
-
config.id,
|
|
104781
|
-
config.config,
|
|
104782
|
-
{
|
|
104783
|
-
agentManager: deps.agentManager,
|
|
104784
|
-
traceStore: deps.traceStore,
|
|
104785
|
-
pulseStore: deps.pulseStore,
|
|
104786
|
-
agentSessionStore: deps.agentSessionStore,
|
|
104787
|
-
logger
|
|
104788
|
-
}
|
|
104789
|
-
);
|
|
104790
|
-
case "plugin":
|
|
104791
|
-
return loadPluginAdapter(config, configPath, onPluginManifest);
|
|
104792
|
-
default:
|
|
104793
|
-
logger.warn(`[AdapterFactory] Unknown adapter type: ${config.type}`);
|
|
104794
|
-
return null;
|
|
105109
|
+
/** Get the BindingRouter, or undefined if initialization did not reach that step. */
|
|
105110
|
+
getBindingRouter() {
|
|
105111
|
+
return this.bindingRouter;
|
|
104795
105112
|
}
|
|
104796
|
-
|
|
104797
|
-
|
|
104798
|
-
|
|
104799
|
-
|
|
104800
|
-
if (
|
|
104801
|
-
|
|
104802
|
-
try {
|
|
104803
|
-
return await Promise.race([
|
|
104804
|
-
adapter.testConnection(),
|
|
104805
|
-
new Promise((_4, reject) => {
|
|
104806
|
-
timer = setTimeout(
|
|
104807
|
-
() => reject(new Error("Connection test timed out")),
|
|
104808
|
-
CONNECTION_TEST_TIMEOUT_MS
|
|
104809
|
-
);
|
|
104810
|
-
})
|
|
104811
|
-
]);
|
|
104812
|
-
} finally {
|
|
104813
|
-
clearTimeout(timer);
|
|
104814
|
-
}
|
|
104815
|
-
}
|
|
104816
|
-
const noopRelay = {
|
|
104817
|
-
publish: async () => ({ messageId: "", deliveredTo: 0 }),
|
|
104818
|
-
onSignal: () => () => {
|
|
104819
|
-
},
|
|
104820
|
-
subscribe: () => () => {
|
|
104821
|
-
}
|
|
104822
|
-
};
|
|
104823
|
-
let fallbackTimer;
|
|
104824
|
-
try {
|
|
104825
|
-
await Promise.race([
|
|
104826
|
-
adapter.start(noopRelay),
|
|
104827
|
-
new Promise((_4, reject) => {
|
|
104828
|
-
fallbackTimer = setTimeout(
|
|
104829
|
-
() => reject(new Error("Connection test timed out")),
|
|
104830
|
-
CONNECTION_TEST_TIMEOUT_MS
|
|
104831
|
-
);
|
|
104832
|
-
})
|
|
104833
|
-
]);
|
|
104834
|
-
} finally {
|
|
104835
|
-
clearTimeout(fallbackTimer);
|
|
104836
|
-
}
|
|
104837
|
-
return { ok: true };
|
|
104838
|
-
} catch (err) {
|
|
104839
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
104840
|
-
return { ok: false, error: message };
|
|
104841
|
-
} finally {
|
|
104842
|
-
try {
|
|
104843
|
-
await adapter.stop();
|
|
104844
|
-
} catch {
|
|
105113
|
+
/** Shut down the BindingRouter, AgentSessionStore, and BindingStore. Idempotent. */
|
|
105114
|
+
async shutdown() {
|
|
105115
|
+
if (this.isShutdown) return;
|
|
105116
|
+
this.isShutdown = true;
|
|
105117
|
+
if (this.bindingRouter) {
|
|
105118
|
+
await this.bindingRouter.shutdown();
|
|
104845
105119
|
}
|
|
105120
|
+
await this.agentSessionStore.shutdown();
|
|
105121
|
+
await this.bindingStore.shutdown();
|
|
104846
105122
|
}
|
|
104847
|
-
}
|
|
104848
|
-
async function loadPluginAdapter(config, configPath, onPluginManifest) {
|
|
104849
|
-
if (!config.plugin) {
|
|
104850
|
-
logger.warn(`[AdapterFactory] Plugin adapter '${config.id}' missing plugin source config`);
|
|
104851
|
-
return null;
|
|
104852
|
-
}
|
|
104853
|
-
const builtinMap = /* @__PURE__ */ new Map();
|
|
104854
|
-
const configDir = dirname5(configPath);
|
|
104855
|
-
const results = await loadAdapters(
|
|
104856
|
-
[
|
|
104857
|
-
{
|
|
104858
|
-
id: config.id,
|
|
104859
|
-
type: config.type,
|
|
104860
|
-
enabled: config.enabled,
|
|
104861
|
-
plugin: config.plugin,
|
|
104862
|
-
config: config.config
|
|
104863
|
-
}
|
|
104864
|
-
],
|
|
104865
|
-
builtinMap,
|
|
104866
|
-
configDir
|
|
104867
|
-
);
|
|
104868
|
-
const result2 = results[0];
|
|
104869
|
-
if (!result2) return null;
|
|
104870
|
-
if (result2.manifest && onPluginManifest) {
|
|
104871
|
-
onPluginManifest(config.type, result2.manifest);
|
|
104872
|
-
}
|
|
104873
|
-
return result2.adapter;
|
|
104874
|
-
}
|
|
105123
|
+
};
|
|
104875
105124
|
|
|
104876
105125
|
// ../../apps/server/src/services/relay/adapter-manager.ts
|
|
104877
105126
|
var AdapterManager = class {
|
|
@@ -104881,9 +105130,7 @@ var AdapterManager = class {
|
|
|
104881
105130
|
configs = [];
|
|
104882
105131
|
deps;
|
|
104883
105132
|
manifests = /* @__PURE__ */ new Map();
|
|
104884
|
-
|
|
104885
|
-
agentSessionStore;
|
|
104886
|
-
bindingRouter;
|
|
105133
|
+
bindingSubsystem;
|
|
104887
105134
|
constructor(registry2, configPath, deps) {
|
|
104888
105135
|
this.registry = registry2;
|
|
104889
105136
|
this.configPath = configPath;
|
|
@@ -104895,56 +105142,30 @@ var AdapterManager = class {
|
|
|
104895
105142
|
await this.enrichManifestsWithDocs();
|
|
104896
105143
|
await ensureDefaultAdapterConfig(this.configPath);
|
|
104897
105144
|
this.configs = await loadAdapterConfig(this.configPath);
|
|
105145
|
+
await this.initBindingSubsystem();
|
|
104898
105146
|
await this.startEnabledAdapters();
|
|
104899
105147
|
this.configWatcher = watchAdapterConfig(this.configPath, () => {
|
|
104900
105148
|
this.reload().catch((err) => {
|
|
104901
105149
|
logger.warn("[AdapterManager] Hot-reload failed:", err);
|
|
104902
105150
|
});
|
|
104903
105151
|
});
|
|
104904
|
-
await this.initBindingSubsystem();
|
|
104905
105152
|
}
|
|
104906
|
-
/** Initialize the binding
|
|
105153
|
+
/** Initialize the binding subsystem. Non-fatal on failure — logs and continues. */
|
|
104907
105154
|
async initBindingSubsystem() {
|
|
104908
|
-
if (!this.deps.relayCore) {
|
|
104909
|
-
logger.info("[AdapterManager] relayCore not provided, skipping binding subsystem");
|
|
105155
|
+
if (!this.deps.relayCore || !this.deps.meshCore) {
|
|
105156
|
+
logger.info("[AdapterManager] relayCore or meshCore not provided, skipping binding subsystem");
|
|
104910
105157
|
return;
|
|
104911
105158
|
}
|
|
104912
|
-
|
|
104913
|
-
|
|
104914
|
-
this.
|
|
104915
|
-
|
|
104916
|
-
|
|
104917
|
-
|
|
104918
|
-
|
|
104919
|
-
|
|
104920
|
-
const agentManager = this.deps.agentManager;
|
|
104921
|
-
const sessionCreator = {
|
|
104922
|
-
async createSession(cwd, permissionMode) {
|
|
104923
|
-
const id = crypto.randomUUID();
|
|
104924
|
-
agentManager.ensureSession(id, { permissionMode: permissionMode ?? "acceptEdits", cwd });
|
|
104925
|
-
return { id };
|
|
104926
|
-
}
|
|
104927
|
-
};
|
|
104928
|
-
if (!this.deps.meshCore) {
|
|
104929
|
-
logger.info("[AdapterManager] meshCore not provided, skipping binding subsystem");
|
|
104930
|
-
return;
|
|
105159
|
+
this.bindingSubsystem = await BindingSubsystem.init({
|
|
105160
|
+
relayCore: this.deps.relayCore,
|
|
105161
|
+
meshCore: this.deps.meshCore,
|
|
105162
|
+
agentManager: this.deps.agentManager,
|
|
105163
|
+
configPath: this.configPath,
|
|
105164
|
+
resolveAdapterInstanceId: (platformType) => {
|
|
105165
|
+
const match = this.configs.find((c3) => c3.type === platformType && c3.enabled);
|
|
105166
|
+
return match?.id;
|
|
104931
105167
|
}
|
|
104932
|
-
|
|
104933
|
-
bindingStore: this.bindingStore,
|
|
104934
|
-
relayCore: this.deps.relayCore,
|
|
104935
|
-
agentManager: sessionCreator,
|
|
104936
|
-
meshCore: this.deps.meshCore,
|
|
104937
|
-
relayDir,
|
|
104938
|
-
resolveAdapterInstanceId: (platformType) => {
|
|
104939
|
-
const match = this.configs.find((c3) => c3.type === platformType && c3.enabled);
|
|
104940
|
-
return match?.id;
|
|
104941
|
-
}
|
|
104942
|
-
});
|
|
104943
|
-
await this.bindingRouter.init();
|
|
104944
|
-
logger.info("[AdapterManager] BindingRouter initialized");
|
|
104945
|
-
} catch (err) {
|
|
104946
|
-
logger.warn("[AdapterManager] Failed to initialize binding subsystem:", err);
|
|
104947
|
-
}
|
|
105168
|
+
});
|
|
104948
105169
|
}
|
|
104949
105170
|
/** Reload config from disk and reconcile adapter state. */
|
|
104950
105171
|
async reload() {
|
|
@@ -105031,15 +105252,15 @@ var AdapterManager = class {
|
|
|
105031
105252
|
}
|
|
105032
105253
|
/** Get the BindingStore, or undefined if binding subsystem was not initialized. */
|
|
105033
105254
|
getBindingStore() {
|
|
105034
|
-
return this.
|
|
105255
|
+
return this.bindingSubsystem?.getBindingStore();
|
|
105035
105256
|
}
|
|
105036
105257
|
/** Get the AgentSessionStore, or undefined if binding subsystem was not initialized. */
|
|
105037
105258
|
getAgentSessionStore() {
|
|
105038
|
-
return this.
|
|
105259
|
+
return this.bindingSubsystem?.getAgentSessionStore();
|
|
105039
105260
|
}
|
|
105040
105261
|
/** Get the BindingRouter, or undefined if binding subsystem was not initialized. */
|
|
105041
105262
|
getBindingRouter() {
|
|
105042
|
-
return this.
|
|
105263
|
+
return this.bindingSubsystem?.getBindingRouter();
|
|
105043
105264
|
}
|
|
105044
105265
|
/** Enrich AdapterContext with Mesh agent info if meshCore is available. */
|
|
105045
105266
|
buildContext(subject) {
|
|
@@ -105147,10 +105368,11 @@ var AdapterManager = class {
|
|
|
105147
105368
|
}
|
|
105148
105369
|
this.configs.splice(index2, 1);
|
|
105149
105370
|
await saveAdapterConfig(this.configPath, this.configs);
|
|
105150
|
-
|
|
105151
|
-
|
|
105371
|
+
const bindingStore = this.bindingSubsystem?.getBindingStore();
|
|
105372
|
+
if (bindingStore) {
|
|
105373
|
+
const orphanBindings = bindingStore.getAll().filter((b3) => b3.adapterId === id);
|
|
105152
105374
|
for (const binding of orphanBindings) {
|
|
105153
|
-
await
|
|
105375
|
+
await bindingStore.delete(binding.id);
|
|
105154
105376
|
}
|
|
105155
105377
|
if (orphanBindings.length > 0) {
|
|
105156
105378
|
logger.info(
|
|
@@ -105189,13 +105411,9 @@ var AdapterManager = class {
|
|
|
105189
105411
|
}
|
|
105190
105412
|
/** Stop all adapters and the config file watcher. */
|
|
105191
105413
|
async shutdown() {
|
|
105192
|
-
if (this.
|
|
105193
|
-
await this.
|
|
105194
|
-
this.
|
|
105195
|
-
}
|
|
105196
|
-
if (this.bindingStore) {
|
|
105197
|
-
await this.bindingStore.shutdown();
|
|
105198
|
-
this.bindingStore = void 0;
|
|
105414
|
+
if (this.bindingSubsystem) {
|
|
105415
|
+
await this.bindingSubsystem.shutdown();
|
|
105416
|
+
this.bindingSubsystem = void 0;
|
|
105199
105417
|
}
|
|
105200
105418
|
if (this.configWatcher) {
|
|
105201
105419
|
await this.configWatcher.close();
|
|
@@ -105229,7 +105447,7 @@ var AdapterManager = class {
|
|
|
105229
105447
|
async buildAdapter(config) {
|
|
105230
105448
|
return createAdapter(
|
|
105231
105449
|
config,
|
|
105232
|
-
{ ...this.deps, agentSessionStore: this.
|
|
105450
|
+
{ ...this.deps, agentSessionStore: this.bindingSubsystem?.getAgentSessionStore() },
|
|
105233
105451
|
this.configPath,
|
|
105234
105452
|
(type, manifest) => this.registerPluginManifest(type, manifest)
|
|
105235
105453
|
);
|
|
@@ -105301,7 +105519,7 @@ var AdapterManager = class {
|
|
|
105301
105519
|
resolveAdapterDocsPath(adapterType) {
|
|
105302
105520
|
const require2 = createRequire(import.meta.url);
|
|
105303
105521
|
const relayEntry = require2.resolve("@dorkos/relay");
|
|
105304
|
-
const distDir =
|
|
105522
|
+
const distDir = dirname7(relayEntry);
|
|
105305
105523
|
return join12(distDir, "adapters", adapterType, "docs");
|
|
105306
105524
|
}
|
|
105307
105525
|
};
|
|
@@ -116188,7 +116406,7 @@ async function start() {
|
|
|
116188
116406
|
adapterRegistry.setLogger(logger);
|
|
116189
116407
|
traceStore = new TraceStore(db);
|
|
116190
116408
|
logger.info("[Relay] TraceStore initialized");
|
|
116191
|
-
relayCore = new RelayCore({ dataDir: relayDataDir, adapterRegistry, db, traceStore });
|
|
116409
|
+
relayCore = new RelayCore({ dataDir: relayDataDir, adapterRegistry, db, traceStore, logger });
|
|
116192
116410
|
await relayCore.registerEndpoint("relay.system.console");
|
|
116193
116411
|
logger.info(`[Relay] RelayCore initialized (dataDir: ${relayDataDir})`);
|
|
116194
116412
|
} catch (err) {
|
|
@@ -116374,7 +116592,10 @@ async function shutdownServices() {
|
|
|
116374
116592
|
}
|
|
116375
116593
|
await tunnelManager.stop();
|
|
116376
116594
|
}
|
|
116595
|
+
var shuttingDown = false;
|
|
116377
116596
|
async function shutdown() {
|
|
116597
|
+
if (shuttingDown) return;
|
|
116598
|
+
shuttingDown = true;
|
|
116378
116599
|
logger.info("Shutting down...");
|
|
116379
116600
|
await shutdownServices();
|
|
116380
116601
|
process.exit(0);
|