switchroom 0.14.36 → 0.14.38
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/agent-scheduler/index.js +6 -8
- package/dist/auth-broker/index.js +6 -8
- package/dist/cli/notion-write-pretool.mjs +6 -8
- package/dist/cli/switchroom.js +82 -52
- package/dist/host-control/main.js +6 -8
- package/dist/vault/approvals/kernel-server.js +6 -8
- package/dist/vault/broker/server.js +6 -8
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +24 -12
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +88 -3
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +11 -1
- package/telegram-plugin/registry/subagents-bugs.test.ts +12 -4
- package/telegram-plugin/subagent-watcher.ts +45 -1
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +73 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +155 -0
|
@@ -11101,18 +11101,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11101
11101
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11102
11102
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11103
11103
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11104
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
11104
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11105
11105
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
11106
11106
|
}).optional().superRefine((tg, ctx) => {
|
|
11107
11107
|
if (!tg)
|
|
11108
11108
|
return;
|
|
11109
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
11110
|
-
ctx.addIssue({
|
|
11111
|
-
code: exports_external.ZodIssueCode.custom,
|
|
11112
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` — supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
11113
|
-
path: ["default_topic_id"]
|
|
11114
|
-
});
|
|
11115
|
-
}
|
|
11116
11109
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
11117
11110
|
ctx.addIssue({
|
|
11118
11111
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -11127,6 +11120,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11127
11120
|
path: ["topic_aliases"]
|
|
11128
11121
|
});
|
|
11129
11122
|
}
|
|
11123
|
+
}).transform((tg) => {
|
|
11124
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
11125
|
+
return { ...tg, default_topic_id: 1 };
|
|
11126
|
+
}
|
|
11127
|
+
return tg;
|
|
11130
11128
|
});
|
|
11131
11129
|
var ChannelsSchema = exports_external.object({
|
|
11132
11130
|
telegram: TelegramChannelSchema
|
|
@@ -11101,18 +11101,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11101
11101
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11102
11102
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11103
11103
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11104
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
11104
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11105
11105
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
11106
11106
|
}).optional().superRefine((tg, ctx) => {
|
|
11107
11107
|
if (!tg)
|
|
11108
11108
|
return;
|
|
11109
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
11110
|
-
ctx.addIssue({
|
|
11111
|
-
code: exports_external.ZodIssueCode.custom,
|
|
11112
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` — supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
11113
|
-
path: ["default_topic_id"]
|
|
11114
|
-
});
|
|
11115
|
-
}
|
|
11116
11109
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
11117
11110
|
ctx.addIssue({
|
|
11118
11111
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -11127,6 +11120,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11127
11120
|
path: ["topic_aliases"]
|
|
11128
11121
|
});
|
|
11129
11122
|
}
|
|
11123
|
+
}).transform((tg) => {
|
|
11124
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
11125
|
+
return { ...tg, default_topic_id: 1 };
|
|
11126
|
+
}
|
|
11127
|
+
return tg;
|
|
11130
11128
|
});
|
|
11131
11129
|
var ChannelsSchema = exports_external.object({
|
|
11132
11130
|
telegram: TelegramChannelSchema
|
|
@@ -11849,18 +11849,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11849
11849
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11850
11850
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11851
11851
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11852
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
11852
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11853
11853
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
|
|
11854
11854
|
}).optional().superRefine((tg, ctx) => {
|
|
11855
11855
|
if (!tg)
|
|
11856
11856
|
return;
|
|
11857
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
11858
|
-
ctx.addIssue({
|
|
11859
|
-
code: exports_external.ZodIssueCode.custom,
|
|
11860
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` \u2014 supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
11861
|
-
path: ["default_topic_id"]
|
|
11862
|
-
});
|
|
11863
|
-
}
|
|
11864
11857
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
11865
11858
|
ctx.addIssue({
|
|
11866
11859
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -11875,6 +11868,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11875
11868
|
path: ["topic_aliases"]
|
|
11876
11869
|
});
|
|
11877
11870
|
}
|
|
11871
|
+
}).transform((tg) => {
|
|
11872
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
11873
|
+
return { ...tg, default_topic_id: 1 };
|
|
11874
|
+
}
|
|
11875
|
+
return tg;
|
|
11878
11876
|
});
|
|
11879
11877
|
var ChannelsSchema = exports_external.object({
|
|
11880
11878
|
telegram: TelegramChannelSchema
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13665,18 +13665,11 @@ var init_schema = __esm(() => {
|
|
|
13665
13665
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
13666
13666
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
13667
13667
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
13668
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
13668
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
13669
13669
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
|
|
13670
13670
|
}).optional().superRefine((tg, ctx) => {
|
|
13671
13671
|
if (!tg)
|
|
13672
13672
|
return;
|
|
13673
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
13674
|
-
ctx.addIssue({
|
|
13675
|
-
code: exports_external.ZodIssueCode.custom,
|
|
13676
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` \u2014 supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
13677
|
-
path: ["default_topic_id"]
|
|
13678
|
-
});
|
|
13679
|
-
}
|
|
13680
13673
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
13681
13674
|
ctx.addIssue({
|
|
13682
13675
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -13691,6 +13684,11 @@ var init_schema = __esm(() => {
|
|
|
13691
13684
|
path: ["topic_aliases"]
|
|
13692
13685
|
});
|
|
13693
13686
|
}
|
|
13687
|
+
}).transform((tg) => {
|
|
13688
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
13689
|
+
return { ...tg, default_topic_id: 1 };
|
|
13690
|
+
}
|
|
13691
|
+
return tg;
|
|
13694
13692
|
});
|
|
13695
13693
|
ChannelsSchema = exports_external.object({
|
|
13696
13694
|
telegram: TelegramChannelSchema
|
|
@@ -23654,7 +23652,7 @@ var init_docker_fleet = __esm(() => {
|
|
|
23654
23652
|
|
|
23655
23653
|
// src/agents/lifecycle.ts
|
|
23656
23654
|
import { execFileSync as execFileSync7, spawn, spawnSync } from "node:child_process";
|
|
23657
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync7, renameSync as
|
|
23655
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync7, renameSync as renameSync4, readFileSync as readFileSync13 } from "node:fs";
|
|
23658
23656
|
import { resolve as resolve13, join as join10 } from "node:path";
|
|
23659
23657
|
import { connect } from "node:net";
|
|
23660
23658
|
function cleanShutdownMarkerPathForAgent(name) {
|
|
@@ -23676,7 +23674,7 @@ function writeRestartReasonMarker(name, reason, opts = {}) {
|
|
|
23676
23674
|
const marker = { ts: Date.now(), signal: "SIGTERM", reason };
|
|
23677
23675
|
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
23678
23676
|
writeFileSync7(tmp, JSON.stringify(marker), "utf-8");
|
|
23679
|
-
|
|
23677
|
+
renameSync4(tmp, path);
|
|
23680
23678
|
} catch {}
|
|
23681
23679
|
}
|
|
23682
23680
|
function buildCliRestartReason(opts) {
|
|
@@ -24154,7 +24152,7 @@ import {
|
|
|
24154
24152
|
mkdirSync as mkdirSync14,
|
|
24155
24153
|
readFileSync as readFileSync17,
|
|
24156
24154
|
readdirSync as readdirSync9,
|
|
24157
|
-
renameSync as
|
|
24155
|
+
renameSync as renameSync6,
|
|
24158
24156
|
rmSync as rmSync4,
|
|
24159
24157
|
statSync as statSync11,
|
|
24160
24158
|
writeFileSync as writeFileSync9
|
|
@@ -24296,7 +24294,7 @@ function atomicCopy(src, dest, mode) {
|
|
|
24296
24294
|
const tmp = `${dest}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`;
|
|
24297
24295
|
try {
|
|
24298
24296
|
writeFileSync9(tmp, contents, { mode });
|
|
24299
|
-
|
|
24297
|
+
renameSync6(tmp, dest);
|
|
24300
24298
|
} catch (err) {
|
|
24301
24299
|
try {
|
|
24302
24300
|
rmSync4(tmp);
|
|
@@ -25835,7 +25833,7 @@ import {
|
|
|
25835
25833
|
mkdirSync as mkdirSync17,
|
|
25836
25834
|
readFileSync as readFileSync22,
|
|
25837
25835
|
readdirSync as readdirSync14,
|
|
25838
|
-
renameSync as
|
|
25836
|
+
renameSync as renameSync7,
|
|
25839
25837
|
rmSync as rmSync10,
|
|
25840
25838
|
statSync as statSync15,
|
|
25841
25839
|
writeFileSync as writeFileSync14
|
|
@@ -25972,7 +25970,7 @@ __export(exports_atomic, {
|
|
|
25972
25970
|
atomicWriteFileSync: () => atomicWriteFileSync2
|
|
25973
25971
|
});
|
|
25974
25972
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
25975
|
-
import { closeSync as closeSync5, constants, fsyncSync as fsyncSync3, openSync as openSync5, renameSync as
|
|
25973
|
+
import { closeSync as closeSync5, constants, fsyncSync as fsyncSync3, openSync as openSync5, renameSync as renameSync8, rmSync as rmSync11, writeSync as writeSync3 } from "node:fs";
|
|
25976
25974
|
function atomicWriteFileSync2(destPath, contents, mode = 384) {
|
|
25977
25975
|
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes3(4).toString("hex")}`;
|
|
25978
25976
|
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
@@ -25983,7 +25981,7 @@ function atomicWriteFileSync2(destPath, contents, mode = 384) {
|
|
|
25983
25981
|
fsyncSync3(fd);
|
|
25984
25982
|
closeSync5(fd);
|
|
25985
25983
|
fd = null;
|
|
25986
|
-
|
|
25984
|
+
renameSync8(tmp, destPath);
|
|
25987
25985
|
} catch (err) {
|
|
25988
25986
|
if (fd !== null) {
|
|
25989
25987
|
try {
|
|
@@ -49440,8 +49438,8 @@ var {
|
|
|
49440
49438
|
} = import__.default;
|
|
49441
49439
|
|
|
49442
49440
|
// src/build-info.ts
|
|
49443
|
-
var VERSION = "0.14.
|
|
49444
|
-
var COMMIT_SHA = "
|
|
49441
|
+
var VERSION = "0.14.38";
|
|
49442
|
+
var COMMIT_SHA = "1529105b";
|
|
49445
49443
|
|
|
49446
49444
|
// src/cli/agent.ts
|
|
49447
49445
|
init_source();
|
|
@@ -49522,6 +49520,7 @@ import {
|
|
|
49522
49520
|
writeFileSync as writeFileSync5,
|
|
49523
49521
|
appendFileSync,
|
|
49524
49522
|
readFileSync as readFileSync11,
|
|
49523
|
+
renameSync as renameSync3,
|
|
49525
49524
|
chmodSync as chmodSync2,
|
|
49526
49525
|
symlinkSync as symlinkSync2,
|
|
49527
49526
|
copyFileSync as copyFileSync4,
|
|
@@ -51637,6 +51636,7 @@ This file is auto-maintained. Do not edit manually.
|
|
|
51637
51636
|
`;
|
|
51638
51637
|
}, created, skipped, 384);
|
|
51639
51638
|
writeIfMissing(join8(agentDir, "telegram", "access.json"), () => buildAccessJson2(agentConfig, telegramConfig, topicId, userId), created, skipped, 384);
|
|
51639
|
+
reconcileConfiguredGroup(join8(agentDir, "telegram", "access.json"), agentConfig, telegramConfig);
|
|
51640
51640
|
if (agentConfig.subagents) {
|
|
51641
51641
|
const agentsDir2 = join8(agentDir, ".claude", "agents");
|
|
51642
51642
|
mkdirSync9(agentsDir2, { recursive: true });
|
|
@@ -52760,6 +52760,36 @@ function rerenderWithFingerprint(filePath, contentFn, created, skipped, rewritte
|
|
|
52760
52760
|
rewrittenWithBackup.push(filePath);
|
|
52761
52761
|
created.push(filePath);
|
|
52762
52762
|
}
|
|
52763
|
+
function resolveAgentForumChatId(agentConfig, telegramConfig) {
|
|
52764
|
+
const override = agentConfig.channels?.telegram?.chat_id;
|
|
52765
|
+
if (typeof override === "string" && override.length > 0)
|
|
52766
|
+
return override;
|
|
52767
|
+
return telegramConfig.forum_chat_id ?? "";
|
|
52768
|
+
}
|
|
52769
|
+
function reconcileConfiguredGroup(accessPath, agentConfig, telegramConfig) {
|
|
52770
|
+
if (!existsSync13(accessPath))
|
|
52771
|
+
return;
|
|
52772
|
+
const forumChatId = resolveAgentForumChatId(agentConfig, telegramConfig);
|
|
52773
|
+
const hasRealForumChat = forumChatId !== "" && forumChatId !== "0";
|
|
52774
|
+
if (agentConfig.dm_only || !hasRealForumChat)
|
|
52775
|
+
return;
|
|
52776
|
+
let access;
|
|
52777
|
+
try {
|
|
52778
|
+
access = JSON.parse(readFileSync11(accessPath, "utf-8"));
|
|
52779
|
+
} catch {
|
|
52780
|
+
return;
|
|
52781
|
+
}
|
|
52782
|
+
const groups = access.groups ??= {};
|
|
52783
|
+
if (groups[forumChatId] !== undefined)
|
|
52784
|
+
return;
|
|
52785
|
+
const allowFrom = Array.isArray(access.allowFrom) ? access.allowFrom : [];
|
|
52786
|
+
groups[forumChatId] = { requireMention: false, allowFrom };
|
|
52787
|
+
const tmp = accessPath + ".tmp";
|
|
52788
|
+
writeFileSync5(tmp, JSON.stringify(access, null, 2) + `
|
|
52789
|
+
`, { mode: 384 });
|
|
52790
|
+
renameSync3(tmp, accessPath);
|
|
52791
|
+
console.log(source_default.green(` registered supergroup ${forumChatId} in access.json ` + `(responds to all topics; requireMention=false)`));
|
|
52792
|
+
}
|
|
52763
52793
|
function buildAccessJson2(agentConfig, telegramConfig, resolvedTopicId, userId) {
|
|
52764
52794
|
const allowFrom = userId ? [String(userId)] : [];
|
|
52765
52795
|
if (allowFrom.length === 0) {
|
|
@@ -52769,7 +52799,7 @@ function buildAccessJson2(agentConfig, telegramConfig, resolvedTopicId, userId)
|
|
|
52769
52799
|
dmPolicy: "allowlist",
|
|
52770
52800
|
allowFrom
|
|
52771
52801
|
};
|
|
52772
|
-
const forumChatId = telegramConfig
|
|
52802
|
+
const forumChatId = resolveAgentForumChatId(agentConfig, telegramConfig);
|
|
52773
52803
|
const hasRealForumChat = forumChatId !== "" && forumChatId !== "0";
|
|
52774
52804
|
if (!agentConfig.dm_only && hasRealForumChat) {
|
|
52775
52805
|
access.groups = {
|
|
@@ -53035,7 +53065,7 @@ import { join as join12 } from "node:path";
|
|
|
53035
53065
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
53036
53066
|
|
|
53037
53067
|
// src/agents/handoff-summarizer.ts
|
|
53038
|
-
import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync as
|
|
53068
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync as renameSync5, mkdirSync as mkdirSync13, existsSync as existsSync16, statSync as statSync9, readdirSync as readdirSync8 } from "node:fs";
|
|
53039
53069
|
import { join as join11 } from "node:path";
|
|
53040
53070
|
var DEFAULT_MAX_TURNS = 50;
|
|
53041
53071
|
var TOPIC_MAX_CHARS = 117;
|
|
@@ -53177,8 +53207,8 @@ function writeSidecarsAtomic(agentDir, briefing, topic) {
|
|
|
53177
53207
|
const topicTmp = topicPath + ".tmp";
|
|
53178
53208
|
writeFileSync8(handoffTmp, briefing, "utf-8");
|
|
53179
53209
|
writeFileSync8(topicTmp, topic, "utf-8");
|
|
53180
|
-
|
|
53181
|
-
|
|
53210
|
+
renameSync5(handoffTmp, handoffPath);
|
|
53211
|
+
renameSync5(topicTmp, topicPath);
|
|
53182
53212
|
}
|
|
53183
53213
|
async function buildHandoff(opts) {
|
|
53184
53214
|
const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
@@ -59057,7 +59087,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
59057
59087
|
init_compose();
|
|
59058
59088
|
init_vault();
|
|
59059
59089
|
import * as net3 from "node:net";
|
|
59060
|
-
import { mkdirSync as mkdirSync22, chmodSync as chmodSync7, chownSync as chownSync2, existsSync as existsSync35, readFileSync as readFileSync30, readdirSync as readdirSync16, statSync as statSync19, unlinkSync as unlinkSync8, writeFileSync as writeFileSync19, renameSync as
|
|
59090
|
+
import { mkdirSync as mkdirSync22, chmodSync as chmodSync7, chownSync as chownSync2, existsSync as existsSync35, readFileSync as readFileSync30, readdirSync as readdirSync16, statSync as statSync19, unlinkSync as unlinkSync8, writeFileSync as writeFileSync19, renameSync as renameSync10 } from "node:fs";
|
|
59061
59091
|
import { dirname as dirname6, resolve as resolve26, basename as basename4 } from "node:path";
|
|
59062
59092
|
import * as os4 from "node:os";
|
|
59063
59093
|
import * as path3 from "node:path";
|
|
@@ -59074,7 +59104,7 @@ import {
|
|
|
59074
59104
|
openSync as openSync6,
|
|
59075
59105
|
closeSync as closeSync6,
|
|
59076
59106
|
readFileSync as readFileSync28,
|
|
59077
|
-
renameSync as
|
|
59107
|
+
renameSync as renameSync9,
|
|
59078
59108
|
statSync as statSync18,
|
|
59079
59109
|
symlinkSync as symlinkSync3,
|
|
59080
59110
|
unlinkSync as unlinkSync7
|
|
@@ -59151,7 +59181,7 @@ function runMigration(home2, opts) {
|
|
|
59151
59181
|
copyFileSync7(oldPath, tempNew);
|
|
59152
59182
|
chmodSync4(tempNew, 384);
|
|
59153
59183
|
fsyncFile(tempNew);
|
|
59154
|
-
|
|
59184
|
+
renameSync9(tempNew, newPath);
|
|
59155
59185
|
fsyncDir(parent);
|
|
59156
59186
|
atomicReplaceWithSymlink(oldPath, "vault/vault.enc");
|
|
59157
59187
|
fsyncDir(switchroomRoot);
|
|
@@ -59240,7 +59270,7 @@ function atomicReplaceWithSymlink(target, linkTarget) {
|
|
|
59240
59270
|
} catch {}
|
|
59241
59271
|
}
|
|
59242
59272
|
symlinkSync3(linkTarget, tmp);
|
|
59243
|
-
|
|
59273
|
+
renameSync9(tmp, target);
|
|
59244
59274
|
}
|
|
59245
59275
|
function fsyncFile(path) {
|
|
59246
59276
|
const fd = openSync6(path, "r+");
|
|
@@ -62880,7 +62910,7 @@ class VaultBroker {
|
|
|
62880
62910
|
const tokenPath = path3.join(tokenDir, ".vault-token");
|
|
62881
62911
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
62882
62912
|
writeFileSync19(tmpPath, mintResult.token, { mode: 384 });
|
|
62883
|
-
|
|
62913
|
+
renameSync10(tmpPath, tokenPath);
|
|
62884
62914
|
try {
|
|
62885
62915
|
const uid = allocateAgentUid(agent);
|
|
62886
62916
|
chownSync2(tokenPath, uid, uid);
|
|
@@ -64262,7 +64292,7 @@ import {
|
|
|
64262
64292
|
openSync as openSync9,
|
|
64263
64293
|
readdirSync as readdirSync17,
|
|
64264
64294
|
readFileSync as readFileSync34,
|
|
64265
|
-
renameSync as
|
|
64295
|
+
renameSync as renameSync11,
|
|
64266
64296
|
statSync as statSync20,
|
|
64267
64297
|
symlinkSync as symlinkSync4,
|
|
64268
64298
|
unlinkSync as unlinkSync10,
|
|
@@ -64414,7 +64444,7 @@ function backupVault(opts) {
|
|
|
64414
64444
|
} catch {}
|
|
64415
64445
|
throw new Error(`vault backup refused: '${fullPath}' already exists ` + `(sub-second collision with another backup). Retry in 1 second, ` + `or check for a concurrent backup process.`);
|
|
64416
64446
|
}
|
|
64417
|
-
|
|
64447
|
+
renameSync11(tmpPath, fullPath);
|
|
64418
64448
|
const stat = statSync20(fullPath);
|
|
64419
64449
|
const sha256 = sha256OfFile(fullPath);
|
|
64420
64450
|
const row = {
|
|
@@ -74106,7 +74136,7 @@ import {
|
|
|
74106
74136
|
openSync as openSync11,
|
|
74107
74137
|
readdirSync as readdirSync21,
|
|
74108
74138
|
readFileSync as readFileSync50,
|
|
74109
|
-
renameSync as
|
|
74139
|
+
renameSync as renameSync12,
|
|
74110
74140
|
statSync as statSync25,
|
|
74111
74141
|
unlinkSync as unlinkSync11,
|
|
74112
74142
|
writeFileSync as writeFileSync26,
|
|
@@ -74664,7 +74694,7 @@ function writeAll(stateDir, events) {
|
|
|
74664
74694
|
`) + `
|
|
74665
74695
|
`;
|
|
74666
74696
|
writeFileSync26(tmp, body, "utf-8");
|
|
74667
|
-
|
|
74697
|
+
renameSync12(tmp, path4);
|
|
74668
74698
|
}
|
|
74669
74699
|
var ORPHAN_TMP_TTL_MS = 60000;
|
|
74670
74700
|
var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
|
|
@@ -76504,7 +76534,7 @@ import {
|
|
|
76504
76534
|
readdirSync as readdirSync23,
|
|
76505
76535
|
unlinkSync as unlinkSync12,
|
|
76506
76536
|
existsSync as existsSync64,
|
|
76507
|
-
renameSync as
|
|
76537
|
+
renameSync as renameSync13
|
|
76508
76538
|
} from "node:fs";
|
|
76509
76539
|
import { join as join63, resolve as resolve41 } from "node:path";
|
|
76510
76540
|
import { homedir as homedir37 } from "node:os";
|
|
@@ -76523,7 +76553,7 @@ function writeRecord(record2) {
|
|
|
76523
76553
|
const tmp = `${target}.tmp${process.pid}`;
|
|
76524
76554
|
writeFileSync30(tmp, JSON.stringify(record2, null, 2) + `
|
|
76525
76555
|
`, { mode: 384 });
|
|
76526
|
-
|
|
76556
|
+
renameSync13(tmp, target);
|
|
76527
76557
|
}
|
|
76528
76558
|
function readRecord(id) {
|
|
76529
76559
|
const path7 = recordPath(id);
|
|
@@ -77842,7 +77872,7 @@ async function fetchToken(vaultKey) {
|
|
|
77842
77872
|
|
|
77843
77873
|
// src/cli/apply.ts
|
|
77844
77874
|
init_source();
|
|
77845
|
-
import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync72, mkdirSync as mkdirSync40, readFileSync as readFileSync57, readdirSync as readdirSync26, renameSync as
|
|
77875
|
+
import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync72, mkdirSync as mkdirSync40, readFileSync as readFileSync57, readdirSync as readdirSync26, renameSync as renameSync14, writeFileSync as writeFileSync35 } from "node:fs";
|
|
77846
77876
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
77847
77877
|
import { spawnSync as childSpawnSync } from "node:child_process";
|
|
77848
77878
|
import readline from "node:readline";
|
|
@@ -78692,7 +78722,7 @@ function writeInstallTypeCache(homeDir = homedir40()) {
|
|
|
78692
78722
|
source_paths: ctx.source_paths
|
|
78693
78723
|
};
|
|
78694
78724
|
writeFileSync35(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
|
|
78695
|
-
|
|
78725
|
+
renameSync14(tmp, out);
|
|
78696
78726
|
return out;
|
|
78697
78727
|
}
|
|
78698
78728
|
async function runApply(config, options, deps = {}, switchroomConfigPath) {
|
|
@@ -79764,7 +79794,7 @@ import {
|
|
|
79764
79794
|
openSync as openSync13,
|
|
79765
79795
|
readdirSync as readdirSync28,
|
|
79766
79796
|
readFileSync as readFileSync60,
|
|
79767
|
-
renameSync as
|
|
79797
|
+
renameSync as renameSync15,
|
|
79768
79798
|
statSync as statSync28,
|
|
79769
79799
|
unlinkSync as unlinkSync14,
|
|
79770
79800
|
writeSync as writeSync8
|
|
@@ -79846,7 +79876,7 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
79846
79876
|
} finally {
|
|
79847
79877
|
closeSync13(fd);
|
|
79848
79878
|
}
|
|
79849
|
-
|
|
79879
|
+
renameSync15(stagingPath, finalPath);
|
|
79850
79880
|
return finalPath;
|
|
79851
79881
|
});
|
|
79852
79882
|
}
|
|
@@ -79863,7 +79893,7 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
79863
79893
|
} finally {
|
|
79864
79894
|
closeSync13(fd);
|
|
79865
79895
|
}
|
|
79866
|
-
|
|
79896
|
+
renameSync15(stagingPath, finalPath);
|
|
79867
79897
|
return finalPath;
|
|
79868
79898
|
});
|
|
79869
79899
|
}
|
|
@@ -80063,7 +80093,7 @@ import {
|
|
|
80063
80093
|
openSync as openSync14,
|
|
80064
80094
|
readdirSync as readdirSync29,
|
|
80065
80095
|
readFileSync as readFileSync61,
|
|
80066
|
-
renameSync as
|
|
80096
|
+
renameSync as renameSync16,
|
|
80067
80097
|
unlinkSync as unlinkSync15,
|
|
80068
80098
|
writeFileSync as writeFileSync36,
|
|
80069
80099
|
writeSync as writeSync9
|
|
@@ -80106,7 +80136,7 @@ function stagePendingScheduleEntry(opts) {
|
|
|
80106
80136
|
} finally {
|
|
80107
80137
|
closeSync14(fd);
|
|
80108
80138
|
}
|
|
80109
|
-
|
|
80139
|
+
renameSync16(yamlTmp, yamlPath);
|
|
80110
80140
|
}
|
|
80111
80141
|
writeFileSync36(metaPath, JSON.stringify(meta, null, 2) + `
|
|
80112
80142
|
`, { mode: 384 });
|
|
@@ -80145,7 +80175,7 @@ function commitPendingScheduleEntry(opts) {
|
|
|
80145
80175
|
if (existsSync76(finalPath)) {
|
|
80146
80176
|
return { committed: false, reason: "slug_collision" };
|
|
80147
80177
|
}
|
|
80148
|
-
|
|
80178
|
+
renameSync16(match.yamlPath, finalPath);
|
|
80149
80179
|
unlinkSync15(match.metaPath);
|
|
80150
80180
|
return { committed: true, path: finalPath, slug };
|
|
80151
80181
|
}
|
|
@@ -80895,7 +80925,7 @@ import {
|
|
|
80895
80925
|
readFileSync as readFileSync63,
|
|
80896
80926
|
readdirSync as readdirSync30,
|
|
80897
80927
|
realpathSync as realpathSync7,
|
|
80898
|
-
renameSync as
|
|
80928
|
+
renameSync as renameSync17,
|
|
80899
80929
|
rmSync as rmSync16,
|
|
80900
80930
|
statSync as statSync29,
|
|
80901
80931
|
writeFileSync as writeFileSync37
|
|
@@ -81350,9 +81380,9 @@ function writePayload(poolDir, name, files) {
|
|
|
81350
81380
|
} catch {}
|
|
81351
81381
|
if (targetExists) {
|
|
81352
81382
|
oldRename = `${target}.skill-apply-old-${Date.now()}`;
|
|
81353
|
-
|
|
81383
|
+
renameSync17(target, oldRename);
|
|
81354
81384
|
}
|
|
81355
|
-
|
|
81385
|
+
renameSync17(staging, target);
|
|
81356
81386
|
if (oldRename) {
|
|
81357
81387
|
rmSync16(oldRename, { recursive: true, force: true });
|
|
81358
81388
|
oldRename = null;
|
|
@@ -81366,7 +81396,7 @@ function writePayload(poolDir, name, files) {
|
|
|
81366
81396
|
if (existsSync79(target)) {
|
|
81367
81397
|
rmSync16(target, { recursive: true, force: true });
|
|
81368
81398
|
}
|
|
81369
|
-
|
|
81399
|
+
renameSync17(oldRename, target);
|
|
81370
81400
|
} catch {}
|
|
81371
81401
|
}
|
|
81372
81402
|
throw err2;
|
|
@@ -81446,7 +81476,7 @@ import {
|
|
|
81446
81476
|
openSync as openSync16,
|
|
81447
81477
|
readFileSync as readFileSync64,
|
|
81448
81478
|
readdirSync as readdirSync31,
|
|
81449
|
-
renameSync as
|
|
81479
|
+
renameSync as renameSync18,
|
|
81450
81480
|
rmSync as rmSync17,
|
|
81451
81481
|
statSync as statSync30,
|
|
81452
81482
|
utimesSync,
|
|
@@ -81509,7 +81539,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
|
|
|
81509
81539
|
sweepMirrorPriors(configSkillsRoot);
|
|
81510
81540
|
if (existsSync80(dest)) {
|
|
81511
81541
|
const trash = join77(configSkillsRoot, `.${name}-trash-${Date.now()}`);
|
|
81512
|
-
|
|
81542
|
+
renameSync18(dest, trash);
|
|
81513
81543
|
}
|
|
81514
81544
|
return;
|
|
81515
81545
|
}
|
|
@@ -81533,9 +81563,9 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
|
|
|
81533
81563
|
walk2(liveSkillDir, staging);
|
|
81534
81564
|
if (existsSync80(dest)) {
|
|
81535
81565
|
const prior = join77(configSkillsRoot, `.${name}-prior-${Date.now()}`);
|
|
81536
|
-
|
|
81566
|
+
renameSync18(dest, prior);
|
|
81537
81567
|
}
|
|
81538
|
-
|
|
81568
|
+
renameSync18(staging, dest);
|
|
81539
81569
|
} catch (err2) {
|
|
81540
81570
|
process.stderr.write(source_default.yellow(`warning: mirror to ${dest} failed (${err2.message ?? err2}); ` + `live copy still works, but this skill is not version-controlled until next successful sync.
|
|
81541
81571
|
`));
|
|
@@ -81718,9 +81748,9 @@ function writePersonalSkill(targetDir, files) {
|
|
|
81718
81748
|
} catch {}
|
|
81719
81749
|
if (targetExists) {
|
|
81720
81750
|
oldRename = `${targetDir}.personal-old-${Date.now()}`;
|
|
81721
|
-
|
|
81751
|
+
renameSync18(targetDir, oldRename);
|
|
81722
81752
|
}
|
|
81723
|
-
|
|
81753
|
+
renameSync18(staging, targetDir);
|
|
81724
81754
|
if (oldRename) {
|
|
81725
81755
|
rmSync17(oldRename, { recursive: true, force: true });
|
|
81726
81756
|
oldRename = null;
|
|
@@ -81734,7 +81764,7 @@ function writePersonalSkill(targetDir, files) {
|
|
|
81734
81764
|
if (existsSync80(targetDir)) {
|
|
81735
81765
|
rmSync17(targetDir, { recursive: true, force: true });
|
|
81736
81766
|
}
|
|
81737
|
-
|
|
81767
|
+
renameSync18(oldRename, targetDir);
|
|
81738
81768
|
} catch {}
|
|
81739
81769
|
}
|
|
81740
81770
|
throw err2;
|
|
@@ -81972,7 +82002,7 @@ function removePersonalAction(name, opts) {
|
|
|
81972
82002
|
mkdirSync45(trashRoot, { recursive: true, mode: 493 });
|
|
81973
82003
|
const ts = Date.now();
|
|
81974
82004
|
const trashTarget = join77(trashRoot, `${name}-${ts}`);
|
|
81975
|
-
|
|
82005
|
+
renameSync18(target, trashTarget);
|
|
81976
82006
|
const now = new Date(ts);
|
|
81977
82007
|
utimesSync(trashTarget, now, now);
|
|
81978
82008
|
mirrorToConfigRepo(agent, name, null);
|
|
@@ -13836,18 +13836,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
13836
13836
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
13837
13837
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
13838
13838
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
13839
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
13839
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
13840
13840
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
13841
13841
|
}).optional().superRefine((tg, ctx) => {
|
|
13842
13842
|
if (!tg)
|
|
13843
13843
|
return;
|
|
13844
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
13845
|
-
ctx.addIssue({
|
|
13846
|
-
code: exports_external.ZodIssueCode.custom,
|
|
13847
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` — supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
13848
|
-
path: ["default_topic_id"]
|
|
13849
|
-
});
|
|
13850
|
-
}
|
|
13851
13844
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
13852
13845
|
ctx.addIssue({
|
|
13853
13846
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -13862,6 +13855,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
13862
13855
|
path: ["topic_aliases"]
|
|
13863
13856
|
});
|
|
13864
13857
|
}
|
|
13858
|
+
}).transform((tg) => {
|
|
13859
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
13860
|
+
return { ...tg, default_topic_id: 1 };
|
|
13861
|
+
}
|
|
13862
|
+
return tg;
|
|
13865
13863
|
});
|
|
13866
13864
|
var ChannelsSchema = exports_external.object({
|
|
13867
13865
|
telegram: TelegramChannelSchema
|
|
@@ -11422,18 +11422,11 @@ var init_schema = __esm(() => {
|
|
|
11422
11422
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11423
11423
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11424
11424
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11425
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
11425
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11426
11426
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
11427
11427
|
}).optional().superRefine((tg, ctx) => {
|
|
11428
11428
|
if (!tg)
|
|
11429
11429
|
return;
|
|
11430
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
11431
|
-
ctx.addIssue({
|
|
11432
|
-
code: exports_external.ZodIssueCode.custom,
|
|
11433
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` — supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
11434
|
-
path: ["default_topic_id"]
|
|
11435
|
-
});
|
|
11436
|
-
}
|
|
11437
11430
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
11438
11431
|
ctx.addIssue({
|
|
11439
11432
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -11448,6 +11441,11 @@ var init_schema = __esm(() => {
|
|
|
11448
11441
|
path: ["topic_aliases"]
|
|
11449
11442
|
});
|
|
11450
11443
|
}
|
|
11444
|
+
}).transform((tg) => {
|
|
11445
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
11446
|
+
return { ...tg, default_topic_id: 1 };
|
|
11447
|
+
}
|
|
11448
|
+
return tg;
|
|
11451
11449
|
});
|
|
11452
11450
|
ChannelsSchema = exports_external.object({
|
|
11453
11451
|
telegram: TelegramChannelSchema
|
|
@@ -11422,18 +11422,11 @@ var init_schema = __esm(() => {
|
|
|
11422
11422
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
11423
11423
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
11424
11424
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
11425
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
11425
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
11426
11426
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
|
|
11427
11427
|
}).optional().superRefine((tg, ctx) => {
|
|
11428
11428
|
if (!tg)
|
|
11429
11429
|
return;
|
|
11430
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
11431
|
-
ctx.addIssue({
|
|
11432
|
-
code: exports_external.ZodIssueCode.custom,
|
|
11433
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` — supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
11434
|
-
path: ["default_topic_id"]
|
|
11435
|
-
});
|
|
11436
|
-
}
|
|
11437
11430
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
11438
11431
|
ctx.addIssue({
|
|
11439
11432
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -11448,6 +11441,11 @@ var init_schema = __esm(() => {
|
|
|
11448
11441
|
path: ["topic_aliases"]
|
|
11449
11442
|
});
|
|
11450
11443
|
}
|
|
11444
|
+
}).transform((tg) => {
|
|
11445
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
11446
|
+
return { ...tg, default_topic_id: 1 };
|
|
11447
|
+
}
|
|
11448
|
+
return tg;
|
|
11451
11449
|
});
|
|
11452
11450
|
ChannelsSchema = exports_external.object({
|
|
11453
11451
|
telegram: TelegramChannelSchema
|
package/package.json
CHANGED
|
@@ -23807,18 +23807,11 @@ var init_schema = __esm(() => {
|
|
|
23807
23807
|
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
|
|
23808
23808
|
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
23809
23809
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
23810
|
-
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves.
|
|
23810
|
+
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
23811
23811
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
|
|
23812
23812
|
}).optional().superRefine((tg, ctx) => {
|
|
23813
23813
|
if (!tg)
|
|
23814
23814
|
return;
|
|
23815
|
-
if (tg.chat_id != null && tg.default_topic_id == null) {
|
|
23816
|
-
ctx.addIssue({
|
|
23817
|
-
code: exports_external.ZodIssueCode.custom,
|
|
23818
|
-
message: "`channels.telegram.chat_id` requires `default_topic_id` \u2014 supergroup-mode agents need a fallback topic for unclassified outbounds.",
|
|
23819
|
-
path: ["default_topic_id"]
|
|
23820
|
-
});
|
|
23821
|
-
}
|
|
23822
23815
|
if (tg.default_topic_id != null && tg.chat_id == null) {
|
|
23823
23816
|
ctx.addIssue({
|
|
23824
23817
|
code: exports_external.ZodIssueCode.custom,
|
|
@@ -23833,6 +23826,11 @@ var init_schema = __esm(() => {
|
|
|
23833
23826
|
path: ["topic_aliases"]
|
|
23834
23827
|
});
|
|
23835
23828
|
}
|
|
23829
|
+
}).transform((tg) => {
|
|
23830
|
+
if (tg && tg.chat_id != null && tg.default_topic_id == null) {
|
|
23831
|
+
return { ...tg, default_topic_id: 1 };
|
|
23832
|
+
}
|
|
23833
|
+
return tg;
|
|
23836
23834
|
});
|
|
23837
23835
|
ChannelsSchema = exports_external.object({
|
|
23838
23836
|
telegram: TelegramChannelSchema
|
|
@@ -49571,6 +49569,20 @@ function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
|
|
|
49571
49569
|
}
|
|
49572
49570
|
db2.prepare("UPDATE subagents SET jsonl_agent_id = ? WHERE id = ?").run(agentId, candidate.id);
|
|
49573
49571
|
log?.(`subagent-watcher: backfill linked ${agentId} \u2192 ${candidate.id}`);
|
|
49572
|
+
try {
|
|
49573
|
+
const linkedRow = db2.prepare("SELECT started_at, parent_turn_key FROM subagents WHERE id = ?").get(candidate.id);
|
|
49574
|
+
if (linkedRow != null && linkedRow.parent_turn_key == null) {
|
|
49575
|
+
const turn = db2.prepare(`SELECT turn_key FROM turns
|
|
49576
|
+
WHERE started_at <= ? AND (ended_at IS NULL OR ended_at >= ?)
|
|
49577
|
+
ORDER BY started_at DESC LIMIT 1`).get(linkedRow.started_at, linkedRow.started_at);
|
|
49578
|
+
if (turn?.turn_key != null) {
|
|
49579
|
+
db2.prepare("UPDATE subagents SET parent_turn_key = ? WHERE id = ?").run(turn.turn_key, candidate.id);
|
|
49580
|
+
log?.(`subagent-watcher: backfill parent_turn_key ${candidate.id} \u2192 ${turn.turn_key}`);
|
|
49581
|
+
}
|
|
49582
|
+
}
|
|
49583
|
+
} catch (err) {
|
|
49584
|
+
log?.(`subagent-watcher: parent_turn_key backfill skipped for ${candidate.id} \u2014 ${err.message}`);
|
|
49585
|
+
}
|
|
49574
49586
|
}
|
|
49575
49587
|
function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, parentStateDir, onUnstall, onFileVanished, onProgress) {
|
|
49576
49588
|
try {
|
|
@@ -51782,10 +51794,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
51782
51794
|
}
|
|
51783
51795
|
|
|
51784
51796
|
// ../src/build-info.ts
|
|
51785
|
-
var VERSION = "0.14.
|
|
51786
|
-
var COMMIT_SHA = "
|
|
51787
|
-
var COMMIT_DATE = "2026-06-
|
|
51788
|
-
var LATEST_PR =
|
|
51797
|
+
var VERSION = "0.14.38";
|
|
51798
|
+
var COMMIT_SHA = "1529105b";
|
|
51799
|
+
var COMMIT_DATE = "2026-06-02T02:25:47Z";
|
|
51800
|
+
var LATEST_PR = 2079;
|
|
51789
51801
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
51790
51802
|
|
|
51791
51803
|
// gateway/boot-version.ts
|
|
@@ -156,6 +156,53 @@ function extractResultSummary(toolResponse) {
|
|
|
156
156
|
return str.slice(0, 200) || null
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Extract the full text of a PostToolUse tool_response (untruncated).
|
|
161
|
+
* Mirrors extractResultSummary's shape handling but returns the whole
|
|
162
|
+
* string so callers can pattern-match on it.
|
|
163
|
+
*/
|
|
164
|
+
function toolResponseText(toolResponse) {
|
|
165
|
+
if (!toolResponse) return ''
|
|
166
|
+
if (Array.isArray(toolResponse.content)) {
|
|
167
|
+
return toolResponse.content
|
|
168
|
+
.filter((c) => c && typeof c === 'object' && c.type === 'text' && typeof c.text === 'string')
|
|
169
|
+
.map((c) => c.text)
|
|
170
|
+
.join('\n')
|
|
171
|
+
}
|
|
172
|
+
if (typeof toolResponse.result === 'string') return toolResponse.result
|
|
173
|
+
if (typeof toolResponse.output === 'string') return toolResponse.output
|
|
174
|
+
if (typeof toolResponse === 'string') return toolResponse
|
|
175
|
+
return ''
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detect Claude Code's async-launch ACK in a PostToolUse tool_response.
|
|
180
|
+
*
|
|
181
|
+
* A `run_in_background` Agent/Task returns IMMEDIATELY with an
|
|
182
|
+
* acknowledgement ("Async agent launched successfully … The agent is working
|
|
183
|
+
* in the background …"), NOT the sub-agent's final result. This ACK is the
|
|
184
|
+
* authoritative, uniform signal that the dispatch was a background one — it is
|
|
185
|
+
* present even when Claude Code omits `run_in_background` from the tool_input
|
|
186
|
+
* the PREtool hook sees (observed on claude-code 2.1.159: a worker whose
|
|
187
|
+
* tool_input lacked the flag still returned this ACK and ran ~3 min past the
|
|
188
|
+
* parent turn, so the pretool recorded background=0 and the worker card never
|
|
189
|
+
* fired). We therefore trust this ACK over the pretool's input-derived flag.
|
|
190
|
+
*
|
|
191
|
+
* Anchored on the specific "async agent launched" phrase (a foreground
|
|
192
|
+
* sub-agent's final report is extremely unlikely to contain it), with a
|
|
193
|
+
* structural backstop ("working in the background" + an agentId token) in case
|
|
194
|
+
* the launch-verb wording drifts. A major wording change degrades to the
|
|
195
|
+
* pretool flag — still correct whenever the model DID pass run_in_background,
|
|
196
|
+
* never worse than before.
|
|
197
|
+
*/
|
|
198
|
+
function isAsyncLaunchAck(toolResponse) {
|
|
199
|
+
const t = toolResponseText(toolResponse).toLowerCase()
|
|
200
|
+
if (!t) return false
|
|
201
|
+
if (t.includes('async agent launched')) return true
|
|
202
|
+
if (t.includes('working in the background') && t.includes('agentid')) return true
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
159
206
|
// ---------------------------------------------------------------------------
|
|
160
207
|
// DB write
|
|
161
208
|
// ---------------------------------------------------------------------------
|
|
@@ -172,9 +219,18 @@ function extractResultSummary(toolResponse) {
|
|
|
172
219
|
* recordSubagentEnd (driven by the JSONL turn_end event) remains the
|
|
173
220
|
* authoritative end-of-life signal.
|
|
174
221
|
*
|
|
222
|
+
* Mis-recorded background (DB background = 0 but `asyncLaunch` is true):
|
|
223
|
+
* Claude Code returned the async-launch ACK even though run_in_background was
|
|
224
|
+
* absent from the tool_input the pretool saw, so the row was wrongly recorded
|
|
225
|
+
* foreground. PROMOTE it to background = 1 and take the background path — do
|
|
226
|
+
* NOT terminalize, because the worker is still running (the ACK is a launch,
|
|
227
|
+
* not a completion). This is the authoritative correction that makes the
|
|
228
|
+
* gateway's worker-feed card fire (onProgress re-reads `background` per tick)
|
|
229
|
+
* AND prevents the premature `completed` the foreground path would write.
|
|
230
|
+
*
|
|
175
231
|
* The done(err | null) callback is invoked after all DB operations complete.
|
|
176
232
|
*/
|
|
177
|
-
function updateRow(dbPath, { id, status, resultSummary, now }, done) {
|
|
233
|
+
function updateRow(dbPath, { id, status, resultSummary, now, asyncLaunch }, done) {
|
|
178
234
|
// SQL to read the background flag so we can choose the right update path.
|
|
179
235
|
const SELECT_SQL = `SELECT background FROM subagents WHERE id = ?`
|
|
180
236
|
|
|
@@ -194,12 +250,22 @@ function updateRow(dbPath, { id, status, resultSummary, now }, done) {
|
|
|
194
250
|
AND status NOT IN ('completed', 'failed')
|
|
195
251
|
`
|
|
196
252
|
|
|
253
|
+
// Promote a mis-recorded foreground row to background (sets background = 1),
|
|
254
|
+
// bumping activity but NOT terminalizing — same shape as BACKGROUND_SQL.
|
|
255
|
+
const PROMOTE_BACKGROUND_SQL = `
|
|
256
|
+
UPDATE subagents
|
|
257
|
+
SET background = 1, result_summary = COALESCE(?, result_summary), last_activity_at = ?
|
|
258
|
+
WHERE id = ?
|
|
259
|
+
AND status NOT IN ('completed', 'failed')
|
|
260
|
+
`
|
|
261
|
+
|
|
197
262
|
// Snapshot all values used inside closures before setImmediate fires.
|
|
198
263
|
const snapDbPath = dbPath
|
|
199
264
|
const snapId = id
|
|
200
265
|
const snapStatus = status
|
|
201
266
|
const snapResultSummary = resultSummary
|
|
202
267
|
const snapNow = now
|
|
268
|
+
const snapAsyncLaunch = asyncLaunch === true
|
|
203
269
|
|
|
204
270
|
// Resolve a synchronous SQLite binding (node:sqlite under Node 22+,
|
|
205
271
|
// bun:sqlite under bun, else null → CLI fallback). See helper docs.
|
|
@@ -216,6 +282,8 @@ function updateRow(dbPath, { id, status, resultSummary, now }, done) {
|
|
|
216
282
|
const isBackground = row != null && row.background === 1
|
|
217
283
|
if (isBackground) {
|
|
218
284
|
db.prepare(BACKGROUND_SQL).run(snapResultSummary, snapNow, snapId)
|
|
285
|
+
} else if (snapAsyncLaunch) {
|
|
286
|
+
db.prepare(PROMOTE_BACKGROUND_SQL).run(snapResultSummary, snapNow, snapId)
|
|
219
287
|
} else {
|
|
220
288
|
db.prepare(FOREGROUND_SQL).run(snapNow, snapStatus, snapResultSummary, snapNow, snapId)
|
|
221
289
|
}
|
|
@@ -239,6 +307,12 @@ function updateRow(dbPath, { id, status, resultSummary, now }, done) {
|
|
|
239
307
|
fillPlaceholders(BACKGROUND_SQL.trim(), [snapResultSummary, snapNow, snapId]),
|
|
240
308
|
done,
|
|
241
309
|
)
|
|
310
|
+
} else if (snapAsyncLaunch) {
|
|
311
|
+
spawnSql(
|
|
312
|
+
snapDbPath,
|
|
313
|
+
fillPlaceholders(PROMOTE_BACKGROUND_SQL.trim(), [snapResultSummary, snapNow, snapId]),
|
|
314
|
+
done,
|
|
315
|
+
)
|
|
242
316
|
} else {
|
|
243
317
|
spawnSql(
|
|
244
318
|
snapDbPath,
|
|
@@ -345,16 +419,26 @@ function main() {
|
|
|
345
419
|
|
|
346
420
|
const toolResponse = event.tool_response ?? null
|
|
347
421
|
|
|
422
|
+
// Authoritative background signal: Claude Code's async-launch ACK. Trusted
|
|
423
|
+
// over the pretool's input-derived flag (which is missing whenever the
|
|
424
|
+
// model/runtime omits run_in_background from tool_input — see
|
|
425
|
+
// isAsyncLaunchAck). Gates both the nudge below and the promote path in
|
|
426
|
+
// updateRow.
|
|
427
|
+
const asyncLaunch = isAsyncLaunchAck(toolResponse)
|
|
428
|
+
|
|
348
429
|
// conversational-pacing beat 4 (foreground half). A foreground
|
|
349
430
|
// sub-agent's PostToolUse fires at real completion, mid-parent-turn,
|
|
350
431
|
// with its result in tool_response — nudge the parent to synthesise a
|
|
351
432
|
// user-facing handback. Background sub-agents are gated OUT: their
|
|
352
433
|
// PostToolUse fires on the launch ACK (BACKGROUND_SQL leaves status
|
|
353
434
|
// untouched for that reason), and their handback is driven by the
|
|
354
|
-
// gateway's subagent-watcher onFinish path instead.
|
|
355
|
-
//
|
|
435
|
+
// gateway's subagent-watcher onFinish path instead. A launch ACK is also
|
|
436
|
+
// gated out via `!asyncLaunch` — at this point the DB flag may still read 0
|
|
437
|
+
// (updateRow promotes it on the next tick), so the ACK is the reliable
|
|
438
|
+
// tell. Fail-silent: an unknown background flag (null) skips the nudge.
|
|
356
439
|
if (
|
|
357
440
|
process.env.SWITCHROOM_SUBAGENT_HANDBACK !== '0'
|
|
441
|
+
&& !asyncLaunch
|
|
358
442
|
&& detectStatus(toolResponse) === 'completed'
|
|
359
443
|
&& readBackgroundFlagSync(dbPath, id) === 0
|
|
360
444
|
) {
|
|
@@ -368,6 +452,7 @@ function main() {
|
|
|
368
452
|
status: detectStatus(toolResponse),
|
|
369
453
|
resultSummary: extractResultSummary(toolResponse),
|
|
370
454
|
now: Date.now(),
|
|
455
|
+
asyncLaunch,
|
|
371
456
|
},
|
|
372
457
|
(err) => {
|
|
373
458
|
if (err) {
|
|
@@ -262,7 +262,17 @@ function main() {
|
|
|
262
262
|
{
|
|
263
263
|
id: event.tool_use_id ?? null,
|
|
264
264
|
parentSessionId: event.session_id ?? null,
|
|
265
|
-
|
|
265
|
+
// parent_turn_key is intentionally NULL here. Claude Code's PreToolUse
|
|
266
|
+
// payload carries its own session id, not the gateway-minted Telegram
|
|
267
|
+
// turn_key (a chat+topic+turn key) the `turns` table is keyed on —
|
|
268
|
+
// `event.turn_id` is always undefined, and even if a future CLI
|
|
269
|
+
// populated it, it would not match a `turns.turn_key`. The gateway
|
|
270
|
+
// resolves parent_turn_key from the
|
|
271
|
+
// sub-agent's started_at at jsonl-link time (subagent-watcher.ts
|
|
272
|
+
// backfillJsonlAgentId), which works even after the parent turn ends.
|
|
273
|
+
// Writing a bogus value here would defeat that backfill's
|
|
274
|
+
// `parent_turn_key IS NULL` guard.
|
|
275
|
+
parentTurnKey: null,
|
|
266
276
|
agentType: input.subagent_type ?? null,
|
|
267
277
|
description: input.description ?? null,
|
|
268
278
|
background: input.run_in_background === true ? 1 : 0,
|
|
@@ -387,8 +387,16 @@ describe('Bug 4 — result_summary always NULL (hook integration)', () => {
|
|
|
387
387
|
|
|
388
388
|
// ─── Bug 5 — parent_turn_key always NULL ─────────────────────────────────────
|
|
389
389
|
|
|
390
|
-
describe('Bug 5 — parent_turn_key
|
|
391
|
-
it('pretool
|
|
390
|
+
describe('Bug 5 — parent_turn_key backfilled by gateway, not the hook', () => {
|
|
391
|
+
it('pretool writes parent_turn_key=NULL even when event.turn_id is present', () => {
|
|
392
|
+
// Claude Code's PreToolUse payload carries its own session id, never the
|
|
393
|
+
// gateway-minted Telegram turn_key (a chat+topic+turn key) the `turns`
|
|
394
|
+
// table is keyed on. `event.turn_id` — even if a future CLI populated it —
|
|
395
|
+
// would not match a `turns.turn_key`, so the hook intentionally writes
|
|
396
|
+
// NULL and lets the gateway backfill parent_turn_key from the sub-agent's
|
|
397
|
+
// started_at at jsonl-link time (subagent-watcher.ts backfillJsonlAgentId).
|
|
398
|
+
// Writing a bogus value here would defeat that backfill's
|
|
399
|
+
// `parent_turn_key IS NULL` guard.
|
|
392
400
|
const event = {
|
|
393
401
|
session_id: 'sess-turnkey',
|
|
394
402
|
turn_id: 'turn-abc-001',
|
|
@@ -406,8 +414,8 @@ describe('Bug 5 — parent_turn_key always NULL (hook integration)', () => {
|
|
|
406
414
|
| undefined
|
|
407
415
|
|
|
408
416
|
expect(row).toBeDefined()
|
|
409
|
-
//
|
|
410
|
-
expect(row!.parent_turn_key).
|
|
417
|
+
// The hook never trusts event.turn_id — gateway backfill owns this column.
|
|
418
|
+
expect(row!.parent_turn_key).toBeNull()
|
|
411
419
|
})
|
|
412
420
|
|
|
413
421
|
it('pretool stores parent_turn_key as NULL when turn_id absent (no regression)', () => {
|
|
@@ -508,7 +508,10 @@ interface FsLike {
|
|
|
508
508
|
* - Row already linked to a different agentId: SQL `WHERE jsonl_agent_id IS
|
|
509
509
|
* NULL` skips it. Re-runs are safe.
|
|
510
510
|
*/
|
|
511
|
-
|
|
511
|
+
// Exported for unit-testing the parent_turn_key backfill (telegram-plugin/
|
|
512
|
+
// tests/subagent-watcher-parent-turn-key.test.ts). Not intended for
|
|
513
|
+
// consumption by other modules.
|
|
514
|
+
export function backfillJsonlAgentId(
|
|
512
515
|
db: SubagentLivenessDb,
|
|
513
516
|
jsonlPath: string,
|
|
514
517
|
agentId: string,
|
|
@@ -555,6 +558,47 @@ function backfillJsonlAgentId(
|
|
|
555
558
|
.prepare('UPDATE subagents SET jsonl_agent_id = ? WHERE id = ?')
|
|
556
559
|
.run(agentId, candidate.id)
|
|
557
560
|
log?.(`subagent-watcher: backfill linked ${agentId} → ${candidate.id}`)
|
|
561
|
+
|
|
562
|
+
// Backfill parent_turn_key (gateway-side). The PreToolUse hook can't know
|
|
563
|
+
// the gateway-minted Telegram turn_key (a chat+topic+turn key) — it only
|
|
564
|
+
// sees Claude Code's session id — so the row was inserted with
|
|
565
|
+
// parent_turn_key=NULL. Resolve
|
|
566
|
+
// it now from the turn whose [started_at, ended_at] window contained the
|
|
567
|
+
// sub-agent's dispatch (its started_at). Keying on the historical
|
|
568
|
+
// started_at, NOT "the turn active now", is what makes this correct for a
|
|
569
|
+
// background worker that outlives its parent turn: the turn may have already
|
|
570
|
+
// ended by link time, but the containment match still finds it. Turns are
|
|
571
|
+
// processed serially per agent, so at most one window contains a given
|
|
572
|
+
// instant; the ORDER BY ... DESC LIMIT 1 is just a defensive tie-break.
|
|
573
|
+
//
|
|
574
|
+
// Without this, resolveSubagentOriginChat() returns null and the live
|
|
575
|
+
// worker card + handback fall back to the operator DM instead of the
|
|
576
|
+
// originating group/forum-topic, and resolveCallingSubagent()'s turn-scoped
|
|
577
|
+
// heuristic (WHERE parent_turn_key = ?) can never see the row. Best-effort:
|
|
578
|
+
// any failure leaves parent_turn_key NULL (today's behaviour) and never
|
|
579
|
+
// throws out of the watcher poll loop.
|
|
580
|
+
try {
|
|
581
|
+
const linkedRow = db
|
|
582
|
+
.prepare('SELECT started_at, parent_turn_key FROM subagents WHERE id = ?')
|
|
583
|
+
.get(candidate.id) as { started_at: number; parent_turn_key: string | null } | null
|
|
584
|
+
if (linkedRow != null && linkedRow.parent_turn_key == null) {
|
|
585
|
+
const turn = db
|
|
586
|
+
.prepare(
|
|
587
|
+
`SELECT turn_key FROM turns
|
|
588
|
+
WHERE started_at <= ? AND (ended_at IS NULL OR ended_at >= ?)
|
|
589
|
+
ORDER BY started_at DESC LIMIT 1`,
|
|
590
|
+
)
|
|
591
|
+
.get(linkedRow.started_at, linkedRow.started_at) as { turn_key: string } | null
|
|
592
|
+
if (turn?.turn_key != null) {
|
|
593
|
+
db
|
|
594
|
+
.prepare('UPDATE subagents SET parent_turn_key = ? WHERE id = ?')
|
|
595
|
+
.run(turn.turn_key, candidate.id)
|
|
596
|
+
log?.(`subagent-watcher: backfill parent_turn_key ${candidate.id} → ${turn.turn_key}`)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (err) {
|
|
600
|
+
log?.(`subagent-watcher: parent_turn_key backfill skipped for ${candidate.id} — ${(err as Error).message}`)
|
|
601
|
+
}
|
|
558
602
|
}
|
|
559
603
|
|
|
560
604
|
// Exported for unit-testing the ENOENT/EACCES deregister path
|
|
@@ -271,6 +271,79 @@ describe('subagent-tracker-posttool', () => {
|
|
|
271
271
|
expect(postResult.status).toBe(0)
|
|
272
272
|
expect(postResult.stdout).not.toContain('additionalContext')
|
|
273
273
|
})
|
|
274
|
+
|
|
275
|
+
// The async-launch ACK is Claude Code's verbatim immediate return for a
|
|
276
|
+
// run_in_background Agent/Task dispatch. The posttool trusts it over the
|
|
277
|
+
// pretool's input-derived background flag, which is missing whenever the
|
|
278
|
+
// runtime omits run_in_background from the tool_input the pretool saw
|
|
279
|
+
// (observed on claude-code 2.1.159 — the clerk worker that never surfaced).
|
|
280
|
+
const ASYNC_LAUNCH_ACK =
|
|
281
|
+
'Async agent launched successfully.\n'
|
|
282
|
+
+ 'agentId: go-live-sync-a176dc93\n'
|
|
283
|
+
+ 'The agent is working in the background. You will be notified '
|
|
284
|
+
+ 'automatically when it completes.'
|
|
285
|
+
|
|
286
|
+
it('promotes a mis-recorded foreground row to background from the launch ACK', () => {
|
|
287
|
+
// Pretool sees NO run_in_background key (the production bug) → records
|
|
288
|
+
// background=0, status=running.
|
|
289
|
+
const preResult = runHook(PRETOOL_SCRIPT, {
|
|
290
|
+
session_id: 's-promote',
|
|
291
|
+
tool_name: 'Agent',
|
|
292
|
+
tool_use_id: 'toolu_promote1',
|
|
293
|
+
tool_input: { subagent_type: 'worker', description: 'Go-live sync' },
|
|
294
|
+
})
|
|
295
|
+
expect(preResult.status).toBe(0)
|
|
296
|
+
|
|
297
|
+
const db = openDb()
|
|
298
|
+
const before = db.prepare('SELECT background, status FROM subagents WHERE id = ?').get('toolu_promote1') as
|
|
299
|
+
| { background: number; status: string }
|
|
300
|
+
| undefined
|
|
301
|
+
expect(before?.background).toBe(0)
|
|
302
|
+
expect(before?.status).toBe('running')
|
|
303
|
+
|
|
304
|
+
// Posttool receives the async-launch ACK → promote to background, do NOT
|
|
305
|
+
// terminalize, and do NOT emit a foreground handback nudge.
|
|
306
|
+
const postResult = runHook(POSTTOOL_SCRIPT, {
|
|
307
|
+
tool_name: 'Agent',
|
|
308
|
+
tool_use_id: 'toolu_promote1',
|
|
309
|
+
tool_response: { content: [{ type: 'text', text: ASYNC_LAUNCH_ACK }] },
|
|
310
|
+
})
|
|
311
|
+
expect(postResult.status).toBe(0)
|
|
312
|
+
expect(postResult.stdout).not.toContain('additionalContext')
|
|
313
|
+
|
|
314
|
+
const after = db.prepare('SELECT background, status, ended_at FROM subagents WHERE id = ?').get('toolu_promote1') as
|
|
315
|
+
| { background: number; status: string; ended_at: number | null }
|
|
316
|
+
| undefined
|
|
317
|
+
expect(after?.background).toBe(1)
|
|
318
|
+
expect(after?.status).toBe('running')
|
|
319
|
+
expect(after?.ended_at == null).toBe(true)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('still terminalizes a genuine foreground completion (no false promote)', () => {
|
|
323
|
+
// A real foreground sub-agent whose final report happens to mention
|
|
324
|
+
// "background" must NOT be mistaken for a launch ACK — the promote path
|
|
325
|
+
// only fires on the specific async-launch phrasing.
|
|
326
|
+
runHook(PRETOOL_SCRIPT, {
|
|
327
|
+
session_id: 's-noflip',
|
|
328
|
+
tool_name: 'Agent',
|
|
329
|
+
tool_use_id: 'toolu_noflip1',
|
|
330
|
+
tool_input: { subagent_type: 'worker', description: 'Real foreground task', run_in_background: false },
|
|
331
|
+
})
|
|
332
|
+
const postResult = runHook(POSTTOOL_SCRIPT, {
|
|
333
|
+
tool_name: 'Agent',
|
|
334
|
+
tool_use_id: 'toolu_noflip1',
|
|
335
|
+
tool_response: { result: 'Done. The feature now runs as a background job.', is_error: false },
|
|
336
|
+
})
|
|
337
|
+
expect(postResult.status).toBe(0)
|
|
338
|
+
expect(postResult.stdout).toContain('additionalContext')
|
|
339
|
+
|
|
340
|
+
const db = openDb()
|
|
341
|
+
const row = db.prepare('SELECT background, status FROM subagents WHERE id = ?').get('toolu_noflip1') as
|
|
342
|
+
| { background: number; status: string }
|
|
343
|
+
| undefined
|
|
344
|
+
expect(row?.background).toBe(0)
|
|
345
|
+
expect(row?.status).toBe('completed')
|
|
346
|
+
})
|
|
274
347
|
})
|
|
275
348
|
|
|
276
349
|
describe('agent-dir resolution (RFC §Bug 2)', () => {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the parent_turn_key backfill in subagent-watcher.ts
|
|
3
|
+
* (backfillJsonlAgentId).
|
|
4
|
+
*
|
|
5
|
+
* The PreToolUse hook records a sub-agent row with parent_turn_key=NULL — it
|
|
6
|
+
* only sees Claude Code's session id, never the Telegram turn_key
|
|
7
|
+
* (chat_id:msg_id) the gateway keys turns on. The gateway backfills
|
|
8
|
+
* parent_turn_key when it links the JSONL stem to the row, resolving it from
|
|
9
|
+
* the turn whose [started_at, ended_at] window contained the sub-agent's
|
|
10
|
+
* dispatch (its started_at). These tests pin that resolution — in particular
|
|
11
|
+
* that it stays correct for a background worker that outlives its parent turn.
|
|
12
|
+
*
|
|
13
|
+
* bun:sqlite — run under Bun:
|
|
14
|
+
* bun test telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
|
|
18
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
19
|
+
import { tmpdir } from 'os'
|
|
20
|
+
import { join } from 'path'
|
|
21
|
+
import { openTurnsDbInMemory } from '../registry/turns-schema.js'
|
|
22
|
+
import { applySubagentsSchema } from '../registry/subagents-schema.js'
|
|
23
|
+
import { backfillJsonlAgentId } from '../subagent-watcher.js'
|
|
24
|
+
|
|
25
|
+
type Db = ReturnType<typeof openTurnsDbInMemory>
|
|
26
|
+
|
|
27
|
+
let tempDir: string
|
|
28
|
+
let db: Db
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tempDir = mkdtempSync(join(tmpdir(), 'sub-parent-turn-'))
|
|
32
|
+
db = openTurnsDbInMemory()
|
|
33
|
+
applySubagentsSchema(db)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
try { db.close() } catch { /* ignore */ }
|
|
38
|
+
try { rmSync(tempDir, { recursive: true, force: true }) } catch { /* ignore */ }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function insertTurn(args: {
|
|
42
|
+
turnKey: string
|
|
43
|
+
chatId: string
|
|
44
|
+
threadId?: string | null
|
|
45
|
+
startedAt: number
|
|
46
|
+
endedAt?: number | null
|
|
47
|
+
}) {
|
|
48
|
+
db.prepare(`
|
|
49
|
+
INSERT INTO turns (turn_key, chat_id, thread_id, started_at, ended_at, created_at, updated_at)
|
|
50
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
51
|
+
`).run(
|
|
52
|
+
args.turnKey,
|
|
53
|
+
args.chatId,
|
|
54
|
+
args.threadId ?? null,
|
|
55
|
+
args.startedAt,
|
|
56
|
+
args.endedAt ?? null,
|
|
57
|
+
args.startedAt,
|
|
58
|
+
args.startedAt,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function insertSub(args: {
|
|
63
|
+
id: string
|
|
64
|
+
agentType: string
|
|
65
|
+
description: string
|
|
66
|
+
startedAt: number
|
|
67
|
+
parentTurnKey?: string | null
|
|
68
|
+
}) {
|
|
69
|
+
db.prepare(`
|
|
70
|
+
INSERT INTO subagents
|
|
71
|
+
(id, parent_session_id, parent_turn_key, agent_type, description,
|
|
72
|
+
background, started_at, last_activity_at, status, jsonl_agent_id)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL)
|
|
74
|
+
`).run(
|
|
75
|
+
args.id,
|
|
76
|
+
'sess-1',
|
|
77
|
+
args.parentTurnKey ?? null,
|
|
78
|
+
args.agentType,
|
|
79
|
+
args.description,
|
|
80
|
+
1,
|
|
81
|
+
args.startedAt,
|
|
82
|
+
args.startedAt,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Write the meta.json the backfill reads to match a row by (agentType, description). */
|
|
87
|
+
function writeMeta(agentType: string, description: string): string {
|
|
88
|
+
const jsonlPath = join(tempDir, 'worker.jsonl')
|
|
89
|
+
writeFileSync(join(tempDir, 'worker.meta.json'), JSON.stringify({ agentType, description }))
|
|
90
|
+
return jsonlPath
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readSub(id: string) {
|
|
94
|
+
return db.prepare('SELECT jsonl_agent_id, parent_turn_key FROM subagents WHERE id = ?').get(id) as
|
|
95
|
+
| { jsonl_agent_id: string | null; parent_turn_key: string | null }
|
|
96
|
+
| undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('backfillJsonlAgentId — parent_turn_key resolution', () => {
|
|
100
|
+
it('resolves parent_turn_key from the turn whose window contains the sub-agent started_at', () => {
|
|
101
|
+
insertTurn({ turnKey: '555:10', chatId: '555', threadId: '42', startedAt: 1000, endedAt: 2000 })
|
|
102
|
+
insertSub({ id: 'toolu_a', agentType: 'worker', description: 'Go-live sync', startedAt: 1500 })
|
|
103
|
+
|
|
104
|
+
const jsonlPath = writeMeta('worker', 'Go-live sync')
|
|
105
|
+
backfillJsonlAgentId(db, jsonlPath, 'agentstem_a')
|
|
106
|
+
|
|
107
|
+
const row = readSub('toolu_a')
|
|
108
|
+
expect(row?.jsonl_agent_id).toBe('agentstem_a')
|
|
109
|
+
expect(row?.parent_turn_key).toBe('555:10')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('resolves to the parent turn even after it has ended (background worker outlives the turn)', () => {
|
|
113
|
+
// Parent turn already ended at 1600; a later turn is active "now".
|
|
114
|
+
insertTurn({ turnKey: '555:10', chatId: '555', startedAt: 1000, endedAt: 1600 })
|
|
115
|
+
insertTurn({ turnKey: '555:20', chatId: '555', startedAt: 1700, endedAt: null })
|
|
116
|
+
// The worker was dispatched at 1500 — inside the FIRST (now-ended) turn.
|
|
117
|
+
insertSub({ id: 'toolu_b', agentType: 'worker', description: 'Long task', startedAt: 1500 })
|
|
118
|
+
|
|
119
|
+
const jsonlPath = writeMeta('worker', 'Long task')
|
|
120
|
+
backfillJsonlAgentId(db, jsonlPath, 'agentstem_b')
|
|
121
|
+
|
|
122
|
+
// Must pick the containing (ended) turn, NOT the turn active at link time.
|
|
123
|
+
expect(readSub('toolu_b')?.parent_turn_key).toBe('555:10')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does NOT overwrite an already-populated parent_turn_key', () => {
|
|
127
|
+
insertTurn({ turnKey: '555:10', chatId: '555', startedAt: 1000, endedAt: 2000 })
|
|
128
|
+
insertSub({
|
|
129
|
+
id: 'toolu_c',
|
|
130
|
+
agentType: 'worker',
|
|
131
|
+
description: 'Preset',
|
|
132
|
+
startedAt: 1500,
|
|
133
|
+
parentTurnKey: '999:9',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const jsonlPath = writeMeta('worker', 'Preset')
|
|
137
|
+
backfillJsonlAgentId(db, jsonlPath, 'agentstem_c')
|
|
138
|
+
|
|
139
|
+
expect(readSub('toolu_c')?.parent_turn_key).toBe('999:9')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('leaves parent_turn_key NULL when no turn window contains the dispatch', () => {
|
|
143
|
+
insertTurn({ turnKey: '555:10', chatId: '555', startedAt: 1000, endedAt: 2000 })
|
|
144
|
+
// Dispatched at 50 — before any turn started.
|
|
145
|
+
insertSub({ id: 'toolu_d', agentType: 'worker', description: 'Orphan', startedAt: 50 })
|
|
146
|
+
|
|
147
|
+
const jsonlPath = writeMeta('worker', 'Orphan')
|
|
148
|
+
backfillJsonlAgentId(db, jsonlPath, 'agentstem_d')
|
|
149
|
+
|
|
150
|
+
const row = readSub('toolu_d')
|
|
151
|
+
// Still linked, but no turn to attribute it to.
|
|
152
|
+
expect(row?.jsonl_agent_id).toBe('agentstem_d')
|
|
153
|
+
expect(row?.parent_turn_key == null).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
})
|