@animalabs/connectome-host 0.3.1
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/.env.example +20 -0
- package/.github/workflows/publish.yml +65 -0
- package/ARCHITECTURE.md +355 -0
- package/CHANGELOG.md +30 -0
- package/HEADLESS-FLEET-PLAN.md +330 -0
- package/README.md +189 -0
- package/UNIFIED-TREE-PLAN.md +242 -0
- package/bun.lock +288 -0
- package/docs/AGENT-MEMORY-GUIDE.md +214 -0
- package/docs/AGENT-ONBOARDING.md +541 -0
- package/docs/ATTENTION-AND-GATING.md +120 -0
- package/docs/DEPLOYMENTS.md +130 -0
- package/docs/DEV-ENVIRONMENT.md +215 -0
- package/docs/LIBRARY-PIPELINE.md +286 -0
- package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
- package/docs/claudeai-evacuation.md +228 -0
- package/docs/debug-context-api.md +208 -0
- package/docs/webui-deployment.md +219 -0
- package/package.json +33 -0
- package/recipes/SETUP.md +308 -0
- package/recipes/TRIUMVIRATE-SETUP.md +467 -0
- package/recipes/claude-export-revive.json +19 -0
- package/recipes/clerk.json +157 -0
- package/recipes/knowledge-miner.json +174 -0
- package/recipes/knowledge-reviewer.json +76 -0
- package/recipes/mcpl-editor-test.json +38 -0
- package/recipes/prompts/transplant-addendum.md +17 -0
- package/recipes/triumvirate.json +63 -0
- package/recipes/webui-fleet-test.json +27 -0
- package/recipes/webui-test.json +16 -0
- package/recipes/zulip-miner.json +87 -0
- package/scripts/connectome-doctor +373 -0
- package/scripts/evacuator.ts +956 -0
- package/scripts/import-claudeai-export.ts +747 -0
- package/scripts/lib/line-reader.ts +53 -0
- package/scripts/test-historical-thinking.ts +148 -0
- package/scripts/warmup-session.ts +336 -0
- package/src/agent-name.ts +58 -0
- package/src/commands.ts +1074 -0
- package/src/headless.ts +528 -0
- package/src/index.ts +867 -0
- package/src/logging-adapter.ts +208 -0
- package/src/mcpl-config.ts +199 -0
- package/src/modules/activity-module.ts +270 -0
- package/src/modules/channel-mode-module.ts +260 -0
- package/src/modules/fleet-module.ts +1705 -0
- package/src/modules/fleet-types.ts +225 -0
- package/src/modules/lessons-module.ts +465 -0
- package/src/modules/mcpl-admin-module.ts +345 -0
- package/src/modules/retrieval-module.ts +327 -0
- package/src/modules/settings-module.ts +143 -0
- package/src/modules/subagent-module.ts +2074 -0
- package/src/modules/subscription-gc-module.ts +322 -0
- package/src/modules/time-module.ts +85 -0
- package/src/modules/tui-module.ts +76 -0
- package/src/modules/web-ui-curve-page.ts +249 -0
- package/src/modules/web-ui-module.ts +2595 -0
- package/src/recipe.ts +1003 -0
- package/src/session-manager.ts +278 -0
- package/src/state/agent-tree-reducer.ts +431 -0
- package/src/state/fleet-tree-aggregator.ts +195 -0
- package/src/strategies/frontdesk-strategy.ts +364 -0
- package/src/synesthete.ts +68 -0
- package/src/tui.ts +2200 -0
- package/src/types/bun-ffi.d.ts +6 -0
- package/src/web/protocol.ts +648 -0
- package/test/agent-name.test.ts +62 -0
- package/test/agent-tree-fleet-launch.test.ts +64 -0
- package/test/agent-tree-reducer-parity.test.ts +194 -0
- package/test/agent-tree-reducer.test.ts +443 -0
- package/test/agent-tree-rollup.test.ts +109 -0
- package/test/autobio-progress-snapshot.test.ts +40 -0
- package/test/claudeai-export-importer.test.ts +386 -0
- package/test/evacuator.test.ts +332 -0
- package/test/fleet-commands.test.ts +244 -0
- package/test/fleet-no-subfleets.test.ts +147 -0
- package/test/fleet-orchestration.test.ts +353 -0
- package/test/fleet-recipe.test.ts +124 -0
- package/test/fleet-route.test.ts +44 -0
- package/test/fleet-smoke.test.ts +479 -0
- package/test/fleet-subscribe-union-e2e.test.ts +123 -0
- package/test/fleet-subscribe-union.test.ts +85 -0
- package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
- package/test/fleet-tree-aggregator.test.ts +215 -0
- package/test/frontdesk-strategy.test.ts +326 -0
- package/test/headless-describe.test.ts +182 -0
- package/test/headless-smoke.test.ts +190 -0
- package/test/logging-adapter.test.ts +87 -0
- package/test/mcpl-admin-module.test.ts +213 -0
- package/test/mcpl-agent-overlay.test.ts +126 -0
- package/test/mock-headless-child.ts +133 -0
- package/test/recipe-cache-ttl.test.ts +37 -0
- package/test/recipe-env.test.ts +157 -0
- package/test/recipe-path-resolution.test.ts +170 -0
- package/test/subagent-async-timeout.test.ts +381 -0
- package/test/subagent-fork-materialise.test.ts +241 -0
- package/test/subagent-peek-scoping.test.ts +180 -0
- package/test/subagent-peek-zombie.test.ts +148 -0
- package/test/subagent-reaper-in-flight.test.ts +229 -0
- package/test/subscription-gc-module.test.ts +136 -0
- package/test/warmup-handoff.test.ts +150 -0
- package/test/web-ui-bind.test.ts +51 -0
- package/test/web-ui-module.test.ts +246 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/tsconfig.json +14 -0
- package/web/bun.lock +357 -0
- package/web/index.html +12 -0
- package/web/package.json +24 -0
- package/web/src/App.tsx +1931 -0
- package/web/src/Context.tsx +158 -0
- package/web/src/ContextDocument.tsx +150 -0
- package/web/src/Files.tsx +271 -0
- package/web/src/Lessons.tsx +164 -0
- package/web/src/Mcpl.tsx +310 -0
- package/web/src/Stream.tsx +119 -0
- package/web/src/TreeSidebar.tsx +283 -0
- package/web/src/Usage.tsx +182 -0
- package/web/src/main.tsx +7 -0
- package/web/src/styles.css +26 -0
- package/web/src/tree.ts +268 -0
- package/web/src/wire.ts +120 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +32 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChannelModeModule — flip a channel between "mentions-only" and
|
|
3
|
+
* "every-message-debounced" attention in one call (item 5).
|
|
4
|
+
*
|
|
5
|
+
* The primitives already exist separately; the value here is bundling the three
|
|
6
|
+
* moves that together define a channel's attention mode so the agent doesn't
|
|
7
|
+
* have to remember (and keep in sync) all three:
|
|
8
|
+
*
|
|
9
|
+
* debounced (wake on every message, batched):
|
|
10
|
+
* 1. subscribe to the channel's ambient traffic (`<toolPrefix>--subscribe_channel`)
|
|
11
|
+
* 2. upsert a per-channel gate policy that DEBOUNCES the ambient tag
|
|
12
|
+
* (framework.addGatePolicy → gate.json, hot-reloaded) so a burst wakes the
|
|
13
|
+
* agent once after it settles
|
|
14
|
+
* 3. pin subscription-gc to "off" for the channel, else a chatty channel
|
|
15
|
+
* auto-unsubscribes itself at the idle-char limit and silently leaves
|
|
16
|
+
* debounced mode
|
|
17
|
+
*
|
|
18
|
+
* mentions (revert to mentions/DMs only):
|
|
19
|
+
* 1. remove the per-channel debounce gate policy
|
|
20
|
+
* 2. unsubscribe from ambient traffic
|
|
21
|
+
* 3. restore subscription-gc to its default limit
|
|
22
|
+
*
|
|
23
|
+
* The SAME `channelId` string is used for all three subsystems — it's the id the
|
|
24
|
+
* gate sees on an incoming event (`GateEventInfo.channelId`) and the id
|
|
25
|
+
* subscription-gc counts/unsubscribes, so they line up by construction (this is
|
|
26
|
+
* exactly how SubscriptionGcModule already operates).
|
|
27
|
+
*
|
|
28
|
+
* Steps are best-effort with per-step reporting rather than a hard transaction:
|
|
29
|
+
* the three subsystems can't be mutated atomically, so on partial failure the
|
|
30
|
+
* tool returns which steps succeeded so the agent (or operator) can reconcile.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type {
|
|
34
|
+
Module,
|
|
35
|
+
ModuleContext,
|
|
36
|
+
ProcessEvent,
|
|
37
|
+
ProcessState,
|
|
38
|
+
EventResponse,
|
|
39
|
+
ToolDefinition,
|
|
40
|
+
ToolCall,
|
|
41
|
+
ToolResult,
|
|
42
|
+
} from '@animalabs/agent-framework';
|
|
43
|
+
import type { AgentFramework } from '@animalabs/agent-framework';
|
|
44
|
+
|
|
45
|
+
export interface ChannelModeConfig {
|
|
46
|
+
/** MCPL server id that owns subscriptions (default 'discord'). */
|
|
47
|
+
serverId?: string;
|
|
48
|
+
/** Tool-name prefix for that server (default `mcpl--<serverId>`). */
|
|
49
|
+
toolPrefix?: string;
|
|
50
|
+
/** subscription-gc module name, for pinning the idle limit (default 'subscription-gc'). */
|
|
51
|
+
gcModuleName?: string;
|
|
52
|
+
/** Default debounce window (ms) when the tool call omits `debounceMs`. */
|
|
53
|
+
defaultDebounceMs?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ChannelMode = 'debounced' | 'mentions';
|
|
57
|
+
|
|
58
|
+
interface StepResult {
|
|
59
|
+
step: string;
|
|
60
|
+
ok: boolean;
|
|
61
|
+
detail: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ChannelModeModule implements Module {
|
|
65
|
+
readonly name = 'channel-mode';
|
|
66
|
+
|
|
67
|
+
private ctx: ModuleContext | null = null;
|
|
68
|
+
private framework: AgentFramework | null = null;
|
|
69
|
+
private callSeq = 0;
|
|
70
|
+
|
|
71
|
+
private readonly serverId: string;
|
|
72
|
+
private readonly toolPrefix: string;
|
|
73
|
+
private readonly gcModuleName: string;
|
|
74
|
+
private readonly defaultDebounceMs: number;
|
|
75
|
+
|
|
76
|
+
constructor(config: ChannelModeConfig = {}) {
|
|
77
|
+
this.serverId = config.serverId ?? 'discord';
|
|
78
|
+
this.toolPrefix = config.toolPrefix ?? `mcpl--${this.serverId}`;
|
|
79
|
+
this.gcModuleName = config.gcModuleName ?? 'subscription-gc';
|
|
80
|
+
this.defaultDebounceMs =
|
|
81
|
+
typeof config.defaultDebounceMs === 'number' && config.defaultDebounceMs > 0
|
|
82
|
+
? Math.round(config.defaultDebounceMs)
|
|
83
|
+
: 180_000;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Wired post-creation (like SubagentModule/ActivityModule) so the module can
|
|
87
|
+
* add/remove gate policies without reaching into the private EventGate. */
|
|
88
|
+
setFramework(framework: AgentFramework): void {
|
|
89
|
+
this.framework = framework;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async start(ctx: ModuleContext): Promise<void> {
|
|
93
|
+
this.ctx = ctx;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async stop(): Promise<void> {
|
|
97
|
+
this.ctx = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getTools(): ToolDefinition[] {
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
name: 'set_channel_mode',
|
|
104
|
+
description:
|
|
105
|
+
'Set how a channel gets your attention, in one step. ' +
|
|
106
|
+
'`mode: "debounced"` = wake on EVERY message but batched: it subscribes ' +
|
|
107
|
+
'to the channel, adds a gate rule that debounces its ambient traffic ' +
|
|
108
|
+
'(one wake after the burst settles), and pins the channel so it is never ' +
|
|
109
|
+
'auto-unsubscribed for being chatty. `mode: "mentions"` reverts to ' +
|
|
110
|
+
'mentions/DMs only: it removes that gate rule, unsubscribes, and restores ' +
|
|
111
|
+
'the default auto-unsubscribe limit. `debounceMs` (100–300000, default ' +
|
|
112
|
+
`${this.defaultDebounceMs}) sets the quiet window for debounced mode.`,
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
channelId: { type: 'string', description: 'Channel id EXACTLY as it appears in incoming events (GateEventInfo.channelId). For Discord that is the MCPL-namespaced id discord:<guildId>:<channelId>, NOT the raw channel snowflake — passing the raw id silently never matches the gate, so debouncing/mode has no effect.' },
|
|
117
|
+
mode: { type: 'string', enum: ['debounced', 'mentions'], description: 'Target attention mode.' },
|
|
118
|
+
debounceMs: { type: 'number', description: 'Quiet window in ms for debounced mode (100–300000).' },
|
|
119
|
+
},
|
|
120
|
+
required: ['channelId', 'mode'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
127
|
+
if (call.name !== 'set_channel_mode') {
|
|
128
|
+
return { success: false, error: `Unknown tool: ${call.name}`, isError: true };
|
|
129
|
+
}
|
|
130
|
+
const input = (call.input ?? {}) as Record<string, unknown>;
|
|
131
|
+
const channelId = input.channelId;
|
|
132
|
+
if (typeof channelId !== 'string' || channelId.length === 0) {
|
|
133
|
+
return { success: false, error: 'channelId is required', isError: true };
|
|
134
|
+
}
|
|
135
|
+
const mode = input.mode;
|
|
136
|
+
if (mode !== 'debounced' && mode !== 'mentions') {
|
|
137
|
+
return { success: false, error: 'mode must be "debounced" or "mentions"', isError: true };
|
|
138
|
+
}
|
|
139
|
+
if (!this.framework) {
|
|
140
|
+
return { success: false, error: 'channel-mode: framework not wired (setFramework not called)', isError: true };
|
|
141
|
+
}
|
|
142
|
+
if (!this.ctx) {
|
|
143
|
+
return { success: false, error: 'channel-mode: module not started', isError: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const debounceMs =
|
|
147
|
+
typeof input.debounceMs === 'number' && input.debounceMs > 0
|
|
148
|
+
? Math.round(input.debounceMs)
|
|
149
|
+
: this.defaultDebounceMs;
|
|
150
|
+
|
|
151
|
+
const steps: StepResult[] =
|
|
152
|
+
mode === 'debounced'
|
|
153
|
+
? await this.applyDebounced(channelId, debounceMs)
|
|
154
|
+
: await this.applyMentions(channelId);
|
|
155
|
+
|
|
156
|
+
const failed = steps.filter((s) => !s.ok);
|
|
157
|
+
const summary =
|
|
158
|
+
failed.length === 0
|
|
159
|
+
? `Channel ${channelId} set to ${mode} mode.`
|
|
160
|
+
: `Channel ${channelId} → ${mode}: ${steps.length - failed.length}/${steps.length} steps ok; ` +
|
|
161
|
+
`failed: ${failed.map((s) => `${s.step} (${s.detail})`).join('; ')}.`;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
success: failed.length === 0,
|
|
165
|
+
isError: failed.length > 0,
|
|
166
|
+
...(failed.length > 0 ? { error: summary } : {}),
|
|
167
|
+
data: { channelId, mode, debounceMs: mode === 'debounced' ? debounceMs : undefined, steps, message: summary },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async onProcess(_event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── mode transitions ──
|
|
176
|
+
|
|
177
|
+
private policyName(channelId: string): string {
|
|
178
|
+
return `channel-mode:debounced:${channelId}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async applyDebounced(channelId: string, debounceMs: number): Promise<StepResult[]> {
|
|
182
|
+
const steps: StepResult[] = [];
|
|
183
|
+
|
|
184
|
+
// 1. Subscribe to ambient traffic.
|
|
185
|
+
steps.push(await this.callToolStep('subscribe', `${this.toolPrefix}--subscribe_channel`, { channelId }));
|
|
186
|
+
|
|
187
|
+
// 2. Upsert the per-channel ambient debounce policy (prepend so it beats a
|
|
188
|
+
// broad ambient defer/debounce — first match wins). Validation lives in
|
|
189
|
+
// the gate; a bad debounceMs surfaces here as this step's failure.
|
|
190
|
+
try {
|
|
191
|
+
this.framework!.addGatePolicy(
|
|
192
|
+
{
|
|
193
|
+
name: this.policyName(channelId),
|
|
194
|
+
match: { source: this.serverId, channel: channelId, tagsAny: ['chat:ambient'] },
|
|
195
|
+
behavior: { debounce: debounceMs },
|
|
196
|
+
},
|
|
197
|
+
{ position: 'prepend' },
|
|
198
|
+
);
|
|
199
|
+
steps.push({ step: 'gate-policy', ok: true, detail: `debounce ${debounceMs}ms on chat:ambient` });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
steps.push({ step: 'gate-policy', ok: false, detail: errMsg(err) });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 3. Pin subscription-gc off so a chatty channel doesn't auto-unsubscribe
|
|
205
|
+
// itself back out of debounced mode.
|
|
206
|
+
steps.push(
|
|
207
|
+
await this.callToolStep('gc-off', `${this.gcModuleName}--set_channel_idle_limit`, {
|
|
208
|
+
channelId,
|
|
209
|
+
limit: 'off',
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return steps;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async applyMentions(channelId: string): Promise<StepResult[]> {
|
|
217
|
+
const steps: StepResult[] = [];
|
|
218
|
+
|
|
219
|
+
// 1. Drop the debounce policy (delivers any pending batch first).
|
|
220
|
+
try {
|
|
221
|
+
const removed = this.framework!.removeGatePolicy(this.policyName(channelId));
|
|
222
|
+
steps.push({ step: 'gate-policy', ok: true, detail: removed ? 'removed' : 'no rule to remove' });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
steps.push({ step: 'gate-policy', ok: false, detail: errMsg(err) });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. Unsubscribe from ambient traffic.
|
|
228
|
+
steps.push(await this.callToolStep('unsubscribe', `${this.toolPrefix}--unsubscribe_channel`, { channelId }));
|
|
229
|
+
|
|
230
|
+
// 3. Restore the default auto-unsubscribe limit.
|
|
231
|
+
steps.push(
|
|
232
|
+
await this.callToolStep('gc-default', `${this.gcModuleName}--set_channel_idle_limit`, {
|
|
233
|
+
channelId,
|
|
234
|
+
limit: 'default',
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return steps;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async callToolStep(
|
|
242
|
+
step: string,
|
|
243
|
+
name: string,
|
|
244
|
+
toolInput: Record<string, unknown>,
|
|
245
|
+
): Promise<StepResult> {
|
|
246
|
+
try {
|
|
247
|
+
const result = await this.ctx!.callTool({ id: `chanmode-${this.callSeq++}`, name, input: toolInput });
|
|
248
|
+
if (result && result.success) {
|
|
249
|
+
return { step, ok: true, detail: name };
|
|
250
|
+
}
|
|
251
|
+
return { step, ok: false, detail: result?.error ?? `${name} returned failure` };
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return { step, ok: false, detail: errMsg(err) };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function errMsg(err: unknown): string {
|
|
259
|
+
return err instanceof Error ? err.message : String(err);
|
|
260
|
+
}
|