@mininglamp-oss/cc-channel-octo 1.0.2 → 1.0.3-dev.1465991
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/CHANGELOG.md +23 -0
- package/config.example.json +5 -0
- package/dist/agent-bridge.js +6 -1
- package/dist/agent-bridge.js.map +1 -1
- package/dist/bot-manager.d.ts +103 -0
- package/dist/bot-manager.js +184 -0
- package/dist/bot-manager.js.map +1 -0
- package/dist/config-watcher.d.ts +48 -0
- package/dist/config-watcher.js +143 -0
- package/dist/config-watcher.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/group-config.d.ts +18 -9
- package/dist/group-config.js +47 -10
- package/dist/group-config.js.map +1 -1
- package/dist/group-context.js +9 -1
- package/dist/group-context.js.map +1 -1
- package/dist/group-md-cache.d.ts +69 -0
- package/dist/group-md-cache.js +93 -0
- package/dist/group-md-cache.js.map +1 -0
- package/dist/group-md-events.d.ts +43 -0
- package/dist/group-md-events.js +43 -0
- package/dist/group-md-events.js.map +1 -0
- package/dist/group-md-tool.d.ts +49 -0
- package/dist/group-md-tool.js +95 -0
- package/dist/group-md-tool.js.map +1 -0
- package/dist/group-md-writeback.d.ts +97 -0
- package/dist/group-md-writeback.js +118 -0
- package/dist/group-md-writeback.js.map +1 -0
- package/dist/group-md.d.ts +53 -0
- package/dist/group-md.js +98 -0
- package/dist/group-md.js.map +1 -0
- package/dist/index.d.ts +15 -30
- package/dist/index.js +133 -140
- package/dist/index.js.map +1 -1
- package/dist/octo/api.d.ts +98 -1
- package/dist/octo/api.js +142 -2
- package/dist/octo/api.js.map +1 -1
- package/dist/octo/channel-id.d.ts +36 -0
- package/dist/octo/channel-id.js +52 -0
- package/dist/octo/channel-id.js.map +1 -0
- package/dist/octo/types.d.ts +22 -0
- package/dist/octo/types.js.map +1 -1
- package/dist/session-router.d.ts +43 -1
- package/dist/session-router.js +79 -3
- package/dist/session-router.js.map +1 -1
- package/dist/session-store.d.ts +26 -0
- package/dist/session-store.js +64 -1
- package/dist/session-store.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.3] - 2026-06-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Hot-reload bots on config change — no gateway restart** — the gateway now
|
|
15
|
+
watches the global `config.json` and loads or unloads bots in place when its
|
|
16
|
+
bot set changes, instead of requiring a full process restart. A bot added by
|
|
17
|
+
provisioning comes online on its own, and a removed bot is disconnected and
|
|
18
|
+
unregistered, without bouncing the gateway or disturbing the other bots'
|
|
19
|
+
live sessions. This pairs with the daemon no longer restarting the gateway on
|
|
20
|
+
provision: previously, after the daemon stopped restarting, a newly added bot
|
|
21
|
+
stayed offline until the gateway was manually restarted.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Bot manager + config watcher** — a serial bot lifecycle manager and a
|
|
26
|
+
debounced config watcher reconcile the running bot set against the on-disk
|
|
27
|
+
config. Batch boot is two-phase (start all, cross-register all, then connect)
|
|
28
|
+
so the cross-bot loop guard is consistent, and removal shuts a bot down before
|
|
29
|
+
unregistering it. A generation guard drops stale reconcile passes when the
|
|
30
|
+
config changes again mid-apply.
|
|
31
|
+
|
|
10
32
|
## [1.0.2] - 2026-06-24
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -386,6 +408,7 @@ hardening across the SSRF, prompt-injection, and protocol-DoS surfaces.
|
|
|
386
408
|
Initial tagged baseline: text messaging, streaming output, SQLite session
|
|
387
409
|
persistence, rate limiting, and the core security model.
|
|
388
410
|
|
|
411
|
+
[1.0.3]: https://github.com/Mininglamp-OSS/cc-channel-octo/compare/v1.0.2...v1.0.3
|
|
389
412
|
[1.0.2]: https://github.com/Mininglamp-OSS/cc-channel-octo/compare/v1.0.1...v1.0.2
|
|
390
413
|
[1.0.1]: https://github.com/Mininglamp-OSS/cc-channel-octo/compare/v1.0.0...v1.0.1
|
|
391
414
|
[1.0.0]: https://github.com/Mininglamp-OSS/cc-channel-octo/compare/v0.2.0...v1.0.0
|
package/config.example.json
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
"_comment_group_config_dir": "v1.0: optional directory of per-group instruction files (<groupId>.md), injected as trusted custom instructions. Operator-controlled — keep it OUTSIDE every bot's workspace (the agent can write there) AND non-writable by the gateway user. Omit to disable.",
|
|
11
11
|
|
|
12
|
+
"_comment_server_md": "P2-A: set serverMd=true to fetch each group's instructions from the server GROUP.md API (GET /v1/bot/groups/{groupNo}/md) first, falling back to the local groupConfigDir file on any failure (404 / network / no content). The fetched copy is cached IN MEMORY ONLY (never on disk — it is injected as a trusted system prompt, and a disk cache the gateway user could write would be a poisoning vector). serverMdTtlMs (default 300000 = 5 min) bounds how stale a cached entry may get before re-fetch. Default serverMd=false = pure local-file behavior (flip off to roll back).",
|
|
13
|
+
|
|
14
|
+
"_comment_md_writeback": "P2-C: set mdWriteback=true to give the agent an in-process MCP tool (mcp__group_md__update_group_md) that writes GROUP.md back to the server (PUT /v1/bot/groups/{groupNo}/md). Invocation is owner-gated (only the bot owner uid may call it); content is capped at 10240 bytes UTF-8 locally; write-backs to the same group are serialized in-process (the server has no compare-and-swap, so this prevents this gateway from racing itself — cross-source last-write-wins still applies). Default false = no write-back tool (GROUP.md is read-only from the gateway). Independent of serverMd.",
|
|
15
|
+
|
|
16
|
+
|
|
12
17
|
"sdk": {
|
|
13
18
|
"_comment_anthropic_base_url": "Q1: Optional self-hosted Claude API gateway base URL. Forwarded to the SDK subprocess (scoped). Must be https:// (or http://localhost for dev) — SSRF-validated at boot. Omit to use Anthropic's public endpoint.",
|
|
14
19
|
"_comment_allowed_tools": "Q2: either '*' (allow every tool the SDK exposes) or an explicit string[] whitelist.",
|
package/dist/agent-bridge.js
CHANGED
|
@@ -269,9 +269,14 @@ export async function* queryAgent(userMessage, config, sessionCtx, onToolUse, op
|
|
|
269
269
|
});
|
|
270
270
|
// Detect the SDK's stale/invalid-resume signal (verified via spike): it throws
|
|
271
271
|
// an Error whose message names the missing/invalid session id.
|
|
272
|
+
// Also catches Bedrock ValidationException for orphaned tool_use blocks left
|
|
273
|
+
// by an interrupted turn (no corresponding tool_result), which surfaces as:
|
|
274
|
+
// ValidationException: messages.N: `tool_use` ids were found without `tool_result` blocks
|
|
275
|
+
// Matching this here causes onResumeFailed() to clear the corrupt sdk_session_id
|
|
276
|
+
// and the turn to retry with fallbackRetryPrompt — same recovery path as stale sessions.
|
|
272
277
|
const isResumeError = (err) => {
|
|
273
278
|
const m = err instanceof Error ? err.message : String(err);
|
|
274
|
-
return /No conversation found with session ID|--resume requires a valid session/i.test(m);
|
|
279
|
+
return /No conversation found with session ID|--resume requires a valid session|tool_use.*ids were found without.*tool_result|tool_result.*blocks.*immediately after/i.test(m);
|
|
275
280
|
};
|
|
276
281
|
const stream = runStream(opts?.resume, userMessage);
|
|
277
282
|
// Drain one SDK stream, yielding assistant text. Reports the SDK session id once
|
package/dist/agent-bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-bridge.js","sourceRoot":"","sources":["../src/agent-bridge.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"agent-bridge.js","sourceRoot":"","sources":["../src/agent-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BotManager — runtime registry of live bots for hot-reload (#157).
|
|
3
|
+
*
|
|
4
|
+
* Owns a long-lived `Map<configId, BotStack>` so the gateway can add/remove a
|
|
5
|
+
* single bot at runtime (driven by a config.json watcher) instead of a full
|
|
6
|
+
* process restart. Holds the two-identity discipline (plan B2): the Map is
|
|
7
|
+
* keyed by configId (config.json `bots[].id`), while the SessionRouter loop
|
|
8
|
+
* guard is keyed by robotUid (Octo register id) — never mixed.
|
|
9
|
+
*
|
|
10
|
+
* Concurrency (plan C/C6): all mutations funnel through a single serial queue
|
|
11
|
+
* (`enqueue`), so a watcher burst can never run two add/remove against the same
|
|
12
|
+
* set concurrently. The diff is computed INSIDE the queued task against the
|
|
13
|
+
* freshly-loaded desired set, never precomputed in the watcher callback.
|
|
14
|
+
*/
|
|
15
|
+
import type { SessionRouter } from './session-router.js';
|
|
16
|
+
/**
|
|
17
|
+
* The subset of a started bot the manager needs to track and tear down. The
|
|
18
|
+
* manager keys bots by the configId passed to addBot (not a field here), and the
|
|
19
|
+
* loop-guard uses robotUid — so configId is intentionally NOT carried on the
|
|
20
|
+
* stack (it would be a write-only field; see plan B2 for the identity split).
|
|
21
|
+
*/
|
|
22
|
+
export interface ManagedBot {
|
|
23
|
+
robotUid: string;
|
|
24
|
+
router: SessionRouter;
|
|
25
|
+
connect: () => Promise<void>;
|
|
26
|
+
shutdown: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Diff a desired config-id set against the currently-running config-id set.
|
|
30
|
+
*
|
|
31
|
+
* Returns the configIds to add (in desired, not running) and to remove (running,
|
|
32
|
+
* not in desired). Order within each list is the input order of `desired` /
|
|
33
|
+
* `running` respectively.
|
|
34
|
+
*/
|
|
35
|
+
export declare function diffBotSets(desired: readonly string[], running: readonly string[]): {
|
|
36
|
+
toAdd: string[];
|
|
37
|
+
toRemove: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Cross-register the loop-guard known-bot uids for a newly-added bot:
|
|
41
|
+
* - the new bot's router learns every existing bot's robotUid, AND
|
|
42
|
+
* - every existing bot's router learns the new bot's robotUid.
|
|
43
|
+
* Bidirectional — a one-way register would let one side treat the other's
|
|
44
|
+
* messages as user input in a mention-free group (plan B). Pure over the
|
|
45
|
+
* passed collections (no I/O), so it is unit-testable.
|
|
46
|
+
*/
|
|
47
|
+
export declare function crossRegisterOnAdd(newBot: ManagedBot, existing: readonly ManagedBot[]): void;
|
|
48
|
+
/** Symmetric teardown: every remaining bot's router forgets the removed bot. */
|
|
49
|
+
export declare function crossUnregisterOnRemove(removed: ManagedBot, remaining: readonly ManagedBot[]): void;
|
|
50
|
+
/** How a managed bot is brought up from a config entry (injected for tests). */
|
|
51
|
+
export type StartFn = (configId: string) => Promise<ManagedBot>;
|
|
52
|
+
/**
|
|
53
|
+
* Runtime registry + serial mutation queue for live bots.
|
|
54
|
+
*
|
|
55
|
+
* The manager never opens sockets itself — `StartFn` (wired to startBot in
|
|
56
|
+
* index.ts) does the register+handler work and returns a ManagedBot whose
|
|
57
|
+
* `connect()` opens the socket. addBot enforces the plan-B ordering:
|
|
58
|
+
* start (register+handler, no socket) → cross-register known bots → connect,
|
|
59
|
+
* with rollback if connect throws.
|
|
60
|
+
*/
|
|
61
|
+
export declare class BotManager {
|
|
62
|
+
private readonly start;
|
|
63
|
+
private readonly bots;
|
|
64
|
+
private queue;
|
|
65
|
+
private shuttingDown;
|
|
66
|
+
constructor(start: StartFn);
|
|
67
|
+
/** Snapshot of currently-running config keys (Map keys). */
|
|
68
|
+
runningKeys(): string[];
|
|
69
|
+
size(): number;
|
|
70
|
+
has(key: string): boolean;
|
|
71
|
+
/** Run a mutation on the serial queue so add/remove never interleave. */
|
|
72
|
+
private enqueue;
|
|
73
|
+
/**
|
|
74
|
+
* Add one bot by configId: start → cross-register → connect, rolling back all
|
|
75
|
+
* three on any failure so a half-added bot never lingers in the Map or in
|
|
76
|
+
* sibling routers' known-bot sets.
|
|
77
|
+
*/
|
|
78
|
+
addBot(configId: string): Promise<void>;
|
|
79
|
+
/** Remove one bot by configId: shutdown + symmetric unregister + drop. */
|
|
80
|
+
removeBot(configId: string): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Bring up several bots as ONE two-phase batch: start every bot (register +
|
|
83
|
+
* handler, NO socket) → cross-register the whole set's loop-guard uids
|
|
84
|
+
* pairwise → connect them all. This preserves the "no bot opens its socket
|
|
85
|
+
* before every sibling knows its robotUid" invariant ACROSS the initial set —
|
|
86
|
+
* adding bots one-by-one via addBot would let the first bot connect before
|
|
87
|
+
* later bots exist, so it would never learn them. A bot that fails to start or
|
|
88
|
+
* connect is rolled back and skipped (resilience); the rest still come up.
|
|
89
|
+
* Returns the configIds that failed.
|
|
90
|
+
*/
|
|
91
|
+
addBotsBatch(configIds: readonly string[]): Promise<{
|
|
92
|
+
failed: {
|
|
93
|
+
configId: string;
|
|
94
|
+
error: unknown;
|
|
95
|
+
}[];
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Shut down every running bot (process exit). Drains them concurrently — order
|
|
99
|
+
* doesn't matter at teardown — and clears the Map. Runs on the serial queue so
|
|
100
|
+
* it can't interleave with an in-flight add/remove.
|
|
101
|
+
*/
|
|
102
|
+
shutdownAll(): Promise<void>;
|
|
103
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff a desired config-id set against the currently-running config-id set.
|
|
3
|
+
*
|
|
4
|
+
* Returns the configIds to add (in desired, not running) and to remove (running,
|
|
5
|
+
* not in desired). Order within each list is the input order of `desired` /
|
|
6
|
+
* `running` respectively.
|
|
7
|
+
*/
|
|
8
|
+
export function diffBotSets(desired, running) {
|
|
9
|
+
const runningSet = new Set(running);
|
|
10
|
+
const desiredSet = new Set(desired);
|
|
11
|
+
const toAdd = desired.filter((id) => !runningSet.has(id));
|
|
12
|
+
const toRemove = running.filter((id) => !desiredSet.has(id));
|
|
13
|
+
return { toAdd, toRemove };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Cross-register the loop-guard known-bot uids for a newly-added bot:
|
|
17
|
+
* - the new bot's router learns every existing bot's robotUid, AND
|
|
18
|
+
* - every existing bot's router learns the new bot's robotUid.
|
|
19
|
+
* Bidirectional — a one-way register would let one side treat the other's
|
|
20
|
+
* messages as user input in a mention-free group (plan B). Pure over the
|
|
21
|
+
* passed collections (no I/O), so it is unit-testable.
|
|
22
|
+
*/
|
|
23
|
+
export function crossRegisterOnAdd(newBot, existing) {
|
|
24
|
+
for (const e of existing) {
|
|
25
|
+
if (e.robotUid === newBot.robotUid)
|
|
26
|
+
continue;
|
|
27
|
+
newBot.router.registerKnownBot(e.robotUid);
|
|
28
|
+
e.router.registerKnownBot(newBot.robotUid);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Symmetric teardown: every remaining bot's router forgets the removed bot. */
|
|
32
|
+
export function crossUnregisterOnRemove(removed, remaining) {
|
|
33
|
+
for (const r of remaining) {
|
|
34
|
+
r.router.unregisterKnownBot(removed.robotUid);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Runtime registry + serial mutation queue for live bots.
|
|
39
|
+
*
|
|
40
|
+
* The manager never opens sockets itself — `StartFn` (wired to startBot in
|
|
41
|
+
* index.ts) does the register+handler work and returns a ManagedBot whose
|
|
42
|
+
* `connect()` opens the socket. addBot enforces the plan-B ordering:
|
|
43
|
+
* start (register+handler, no socket) → cross-register known bots → connect,
|
|
44
|
+
* with rollback if connect throws.
|
|
45
|
+
*/
|
|
46
|
+
export class BotManager {
|
|
47
|
+
start;
|
|
48
|
+
bots = new Map();
|
|
49
|
+
queue = Promise.resolve();
|
|
50
|
+
// Set by shutdownAll() so any apply task that was already past the watcher's
|
|
51
|
+
// `closed` guard can't re-add a bot after teardown has begun (plan E / N4
|
|
52
|
+
// shutdown race). Once true, addBot is a permanent no-op.
|
|
53
|
+
shuttingDown = false;
|
|
54
|
+
constructor(start) {
|
|
55
|
+
this.start = start;
|
|
56
|
+
}
|
|
57
|
+
/** Snapshot of currently-running config keys (Map keys). */
|
|
58
|
+
runningKeys() {
|
|
59
|
+
return [...this.bots.keys()];
|
|
60
|
+
}
|
|
61
|
+
size() {
|
|
62
|
+
return this.bots.size;
|
|
63
|
+
}
|
|
64
|
+
has(key) {
|
|
65
|
+
return this.bots.has(key);
|
|
66
|
+
}
|
|
67
|
+
/** Run a mutation on the serial queue so add/remove never interleave. */
|
|
68
|
+
enqueue(task) {
|
|
69
|
+
const run = this.queue.then(task, task);
|
|
70
|
+
// Keep the chain alive even if a task rejects (so later tasks still run),
|
|
71
|
+
// but propagate the result/rejection to this call's awaiter.
|
|
72
|
+
this.queue = run.then(() => undefined, () => undefined);
|
|
73
|
+
return run;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Add one bot by configId: start → cross-register → connect, rolling back all
|
|
77
|
+
* three on any failure so a half-added bot never lingers in the Map or in
|
|
78
|
+
* sibling routers' known-bot sets.
|
|
79
|
+
*/
|
|
80
|
+
addBot(configId) {
|
|
81
|
+
return this.enqueue(async () => {
|
|
82
|
+
if (this.shuttingDown)
|
|
83
|
+
return; // teardown started — don't resurrect bots
|
|
84
|
+
if (this.bots.has(configId))
|
|
85
|
+
return; // idempotent — already running
|
|
86
|
+
const bot = await this.start(configId);
|
|
87
|
+
const existing = [...this.bots.values()];
|
|
88
|
+
crossRegisterOnAdd(bot, existing);
|
|
89
|
+
try {
|
|
90
|
+
await bot.connect();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Roll back: undo the cross-registration, tear the bot down, leave the
|
|
94
|
+
// set exactly as it was before this addBot.
|
|
95
|
+
crossUnregisterOnRemove(bot, existing);
|
|
96
|
+
await bot.shutdown().catch(() => { });
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
this.bots.set(configId, bot);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Remove one bot by configId: shutdown + symmetric unregister + drop. */
|
|
103
|
+
removeBot(configId) {
|
|
104
|
+
return this.enqueue(async () => {
|
|
105
|
+
const bot = this.bots.get(configId);
|
|
106
|
+
if (!bot)
|
|
107
|
+
return; // idempotent — already gone
|
|
108
|
+
this.bots.delete(configId);
|
|
109
|
+
const remaining = [...this.bots.values()];
|
|
110
|
+
// Symmetric inverse of addBot's ordering: close the socket FIRST, then let
|
|
111
|
+
// siblings forget this bot. If we unregistered before shutdown, the bot's
|
|
112
|
+
// in-flight handlers (still draining for up to the gateway drain timeout)
|
|
113
|
+
// could emit replies that siblings — having already dropped its robotUid —
|
|
114
|
+
// would treat as user input, reopening the very loop the guard prevents.
|
|
115
|
+
await bot.shutdown().catch(() => { });
|
|
116
|
+
crossUnregisterOnRemove(bot, remaining);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Bring up several bots as ONE two-phase batch: start every bot (register +
|
|
121
|
+
* handler, NO socket) → cross-register the whole set's loop-guard uids
|
|
122
|
+
* pairwise → connect them all. This preserves the "no bot opens its socket
|
|
123
|
+
* before every sibling knows its robotUid" invariant ACROSS the initial set —
|
|
124
|
+
* adding bots one-by-one via addBot would let the first bot connect before
|
|
125
|
+
* later bots exist, so it would never learn them. A bot that fails to start or
|
|
126
|
+
* connect is rolled back and skipped (resilience); the rest still come up.
|
|
127
|
+
* Returns the configIds that failed.
|
|
128
|
+
*/
|
|
129
|
+
addBotsBatch(configIds) {
|
|
130
|
+
return this.enqueue(async () => {
|
|
131
|
+
const failed = [];
|
|
132
|
+
if (this.shuttingDown) {
|
|
133
|
+
return { failed: configIds.map((configId) => ({ configId, error: new Error('shutting down') })) };
|
|
134
|
+
}
|
|
135
|
+
// Phase 1: start (register + handler, no socket) every not-yet-running bot.
|
|
136
|
+
const started = [];
|
|
137
|
+
for (const configId of configIds) {
|
|
138
|
+
if (this.bots.has(configId))
|
|
139
|
+
continue; // already running — skip
|
|
140
|
+
try {
|
|
141
|
+
started.push({ configId, bot: await this.start(configId) });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
failed.push({ configId, error });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Phase 2: cross-register loop-guard uids across the existing set AND the
|
|
148
|
+
// whole freshly-started batch, BEFORE any socket opens.
|
|
149
|
+
const existing = [...this.bots.values()];
|
|
150
|
+
const batch = started.map((s) => s.bot);
|
|
151
|
+
for (const s of started) {
|
|
152
|
+
crossRegisterOnAdd(s.bot, [...existing, ...batch]);
|
|
153
|
+
}
|
|
154
|
+
// Phase 3: connect every started bot. A connect failure rolls back just
|
|
155
|
+
// that bot (unregister from everyone + shutdown) and is reported.
|
|
156
|
+
for (const s of started) {
|
|
157
|
+
try {
|
|
158
|
+
await s.bot.connect();
|
|
159
|
+
this.bots.set(s.configId, s.bot);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
crossUnregisterOnRemove(s.bot, [...existing, ...batch.filter((b) => b !== s.bot)]);
|
|
163
|
+
await s.bot.shutdown().catch(() => { });
|
|
164
|
+
failed.push({ configId: s.configId, error });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { failed };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Shut down every running bot (process exit). Drains them concurrently — order
|
|
172
|
+
* doesn't matter at teardown — and clears the Map. Runs on the serial queue so
|
|
173
|
+
* it can't interleave with an in-flight add/remove.
|
|
174
|
+
*/
|
|
175
|
+
shutdownAll() {
|
|
176
|
+
this.shuttingDown = true;
|
|
177
|
+
return this.enqueue(async () => {
|
|
178
|
+
const all = [...this.bots.values()];
|
|
179
|
+
this.bots.clear();
|
|
180
|
+
await Promise.allSettled(all.map((b) => b.shutdown()));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=bot-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bot-manager.js","sourceRoot":"","sources":["../src/bot-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Minimal manager surface the watcher needs (eases testing). */
|
|
2
|
+
export interface Reconcilable {
|
|
3
|
+
runningKeys(): string[];
|
|
4
|
+
addBot(configId: string): Promise<void>;
|
|
5
|
+
removeBot(configId: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Reconcile the running set toward `desiredConfigIds` once. Removes first, then
|
|
9
|
+
* adds, so a config that swaps a bot for another with an overlapping resource
|
|
10
|
+
* (lock/dir) frees it before the replacement claims it. Add/remove failures are
|
|
11
|
+
* logged and swallowed so one bad bot doesn't abort the whole reconcile; the
|
|
12
|
+
* BotManager queue keeps the set consistent. Pure-ish (no fs) for unit testing.
|
|
13
|
+
*
|
|
14
|
+
* `isStale` is checked before each add/remove: a newer config event makes the
|
|
15
|
+
* in-flight reconcile abandon its remaining (now outdated) actions, so a bot
|
|
16
|
+
* removed by a later edit is never started by an earlier, slower reconcile
|
|
17
|
+
* (plan C6 generation guard).
|
|
18
|
+
*/
|
|
19
|
+
export declare function reconcile(manager: Reconcilable, desiredConfigIds: readonly string[], log?: (msg: string) => void, isStale?: () => boolean): Promise<void>;
|
|
20
|
+
export interface WatcherHandle {
|
|
21
|
+
/** Force an immediate reconcile (bypasses debounce). For tests / initial sync. */
|
|
22
|
+
applyNow(): Promise<void>;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
export interface WatchOptions {
|
|
26
|
+
/** Absolute path to the global config.json. */
|
|
27
|
+
configPath: string;
|
|
28
|
+
manager: Reconcilable;
|
|
29
|
+
/**
|
|
30
|
+
* Produce the desired configId list from disk. MUST throw on any invalid
|
|
31
|
+
* config so the watcher can keep the current set (plan D). Typically wraps
|
|
32
|
+
* loadConfig + resolveBotConfigs and maps to `bots[].id`.
|
|
33
|
+
*/
|
|
34
|
+
loadDesired: () => readonly string[];
|
|
35
|
+
/** Debounce window in ms (coalesce a burst of writes). Default 200. */
|
|
36
|
+
debounceMs?: number;
|
|
37
|
+
log?: (msg: string) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start watching `configPath`'s directory and reconcile on change.
|
|
41
|
+
*
|
|
42
|
+
* Serialization & staleness: a single `chain` promise serializes apply runs,
|
|
43
|
+
* and each run re-reads the latest config at execution time, so a burst of
|
|
44
|
+
* events collapses to "apply the newest state" rather than replaying each
|
|
45
|
+
* intermediate edit. The BotManager's own queue further serializes the
|
|
46
|
+
* resulting add/remove calls.
|
|
47
|
+
*/
|
|
48
|
+
export declare function watchConfig(opts: WatchOptions): WatcherHandle;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config watcher — drives BotManager from config.json changes (#157).
|
|
3
|
+
*
|
|
4
|
+
* Watches the global config's directory (not the file directly: an atomic
|
|
5
|
+
* temp+rename swaps the inode, and a file-targeted fs.watch would stop firing
|
|
6
|
+
* after the first rename). On any change it schedules a debounced, serialized
|
|
7
|
+
* `applyLatestConfig`, which RE-READS the latest config inside the task (never
|
|
8
|
+
* a precomputed diff — plan C6) and reconciles the running set toward it.
|
|
9
|
+
*
|
|
10
|
+
* Robustness (plan D): the desired set is produced by a single `loadDesired`
|
|
11
|
+
* call; if it throws for ANY reason (half-written file, JSON error, missing
|
|
12
|
+
* token, duplicate id, unsafe apiUrl, broken per-bot config), the current
|
|
13
|
+
* running set is left untouched — a bad edit never tears down healthy bots.
|
|
14
|
+
*/
|
|
15
|
+
import { watch } from 'node:fs';
|
|
16
|
+
import { dirname, basename } from 'node:path';
|
|
17
|
+
import { diffBotSets } from './bot-manager.js';
|
|
18
|
+
/**
|
|
19
|
+
* Reconcile the running set toward `desiredConfigIds` once. Removes first, then
|
|
20
|
+
* adds, so a config that swaps a bot for another with an overlapping resource
|
|
21
|
+
* (lock/dir) frees it before the replacement claims it. Add/remove failures are
|
|
22
|
+
* logged and swallowed so one bad bot doesn't abort the whole reconcile; the
|
|
23
|
+
* BotManager queue keeps the set consistent. Pure-ish (no fs) for unit testing.
|
|
24
|
+
*
|
|
25
|
+
* `isStale` is checked before each add/remove: a newer config event makes the
|
|
26
|
+
* in-flight reconcile abandon its remaining (now outdated) actions, so a bot
|
|
27
|
+
* removed by a later edit is never started by an earlier, slower reconcile
|
|
28
|
+
* (plan C6 generation guard).
|
|
29
|
+
*/
|
|
30
|
+
export async function reconcile(manager, desiredConfigIds, log = () => { }, isStale = () => false) {
|
|
31
|
+
const { toAdd, toRemove } = diffBotSets(desiredConfigIds, manager.runningKeys());
|
|
32
|
+
for (const id of toRemove) {
|
|
33
|
+
if (isStale()) {
|
|
34
|
+
log(`[hot-reload] reconcile superseded by a newer config, stopping`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await manager.removeBot(id);
|
|
39
|
+
log(`[hot-reload] removed bot ${id}`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
log(`[hot-reload] removeBot ${id} failed: ${errMsg(err)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const id of toAdd) {
|
|
46
|
+
if (isStale()) {
|
|
47
|
+
log(`[hot-reload] reconcile superseded by a newer config, stopping`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await manager.addBot(id);
|
|
52
|
+
log(`[hot-reload] added bot ${id}`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
log(`[hot-reload] addBot ${id} failed: ${errMsg(err)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function errMsg(err) {
|
|
60
|
+
return err instanceof Error ? err.message : String(err);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Start watching `configPath`'s directory and reconcile on change.
|
|
64
|
+
*
|
|
65
|
+
* Serialization & staleness: a single `chain` promise serializes apply runs,
|
|
66
|
+
* and each run re-reads the latest config at execution time, so a burst of
|
|
67
|
+
* events collapses to "apply the newest state" rather than replaying each
|
|
68
|
+
* intermediate edit. The BotManager's own queue further serializes the
|
|
69
|
+
* resulting add/remove calls.
|
|
70
|
+
*/
|
|
71
|
+
export function watchConfig(opts) {
|
|
72
|
+
const { configPath, manager, loadDesired } = opts;
|
|
73
|
+
const debounceMs = opts.debounceMs ?? 200;
|
|
74
|
+
const log = opts.log ?? (() => { });
|
|
75
|
+
const dir = dirname(configPath);
|
|
76
|
+
const file = basename(configPath);
|
|
77
|
+
let chain = Promise.resolve();
|
|
78
|
+
let timer;
|
|
79
|
+
let closed = false;
|
|
80
|
+
// Generation guard (plan C6). `latestGen` is bumped the moment a file event is
|
|
81
|
+
// observed (in schedule), NOT when the debounced apply finally runs — so an
|
|
82
|
+
// in-flight reconcile is invalidated immediately on a new event, even during
|
|
83
|
+
// the debounce window, instead of continuing to add/connect a bot a later
|
|
84
|
+
// edit already removed. Each apply captures the gen current when it starts and
|
|
85
|
+
// bails (isStale) as soon as a newer event has bumped past it.
|
|
86
|
+
let latestGen = 0;
|
|
87
|
+
const apply = () => {
|
|
88
|
+
const myGen = latestGen;
|
|
89
|
+
// Re-read latest desired set INSIDE the serialized task (plan C6). Any
|
|
90
|
+
// failure (half-write / invalid config) leaves the running set untouched.
|
|
91
|
+
chain = chain.then(async () => {
|
|
92
|
+
if (closed)
|
|
93
|
+
return;
|
|
94
|
+
let desired;
|
|
95
|
+
try {
|
|
96
|
+
desired = loadDesired();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log(`[hot-reload] config invalid, keeping current bots: ${errMsg(err)}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await reconcile(manager, desired, log, () => closed || myGen !== latestGen);
|
|
103
|
+
});
|
|
104
|
+
return chain;
|
|
105
|
+
};
|
|
106
|
+
const schedule = () => {
|
|
107
|
+
if (closed)
|
|
108
|
+
return;
|
|
109
|
+
// Invalidate any in-flight reconcile right now (not at debounce expiry): a
|
|
110
|
+
// slow reconcile must see this newer event before it acts on stale desired.
|
|
111
|
+
latestGen++;
|
|
112
|
+
if (timer)
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
timer = undefined;
|
|
116
|
+
void apply();
|
|
117
|
+
}, debounceMs);
|
|
118
|
+
timer.unref?.();
|
|
119
|
+
};
|
|
120
|
+
let watcher;
|
|
121
|
+
try {
|
|
122
|
+
watcher = watch(dir, (_event, changed) => {
|
|
123
|
+
// Only react to our config file (the dir may hold per-bot subdirs etc.).
|
|
124
|
+
// changed is null on some platforms — be permissive and reconcile then.
|
|
125
|
+
if (changed === null || changed === file)
|
|
126
|
+
schedule();
|
|
127
|
+
});
|
|
128
|
+
watcher.on('error', (err) => log(`[hot-reload] watcher error: ${errMsg(err)}`));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
log(`[hot-reload] failed to start watcher on ${dir}: ${errMsg(err)}`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
applyNow: apply,
|
|
135
|
+
close: () => {
|
|
136
|
+
closed = true;
|
|
137
|
+
if (timer)
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
watcher?.close();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=config-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.js","sourceRoot":"","sources":["../src/config-watcher.ts"],"names":[],"mappings":"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"}
|