channels.tools 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/index.test.ts +37 -1
- package/src/index.ts +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "channels.tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Channels for the pi coding agent: connect channel servers that push events into a live session — wake it when idle, queue when busy — and proxy their tools. A client for the claude/channel convention.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.test.ts
CHANGED
|
@@ -30,7 +30,11 @@ function fakePi() {
|
|
|
30
30
|
sendMessage(message: unknown, opts: unknown) { customs.push({ msg: message, opts }); },
|
|
31
31
|
registerTool(def: ToolDef) { registered.push(def.name); tools.set(def.name, def); },
|
|
32
32
|
unregisterTool(name: string) { unregistered.push(name); tools.delete(name); },
|
|
33
|
-
|
|
33
|
+
// Faithful to pi 0.84.4: getAllTools() reads the full _toolDefinitions
|
|
34
|
+
// registry, so a tool this extension registered on a prior pass is still
|
|
35
|
+
// returned here (there is no working unregisterTool to evict it). A static
|
|
36
|
+
// [bash, read] would hide the reload self-collision this guards against.
|
|
37
|
+
getAllTools() { return [{ name: "bash" }, { name: "read" }, ...[...tools.keys()].map((name) => ({ name }))]; },
|
|
34
38
|
getActiveTools() { return ["bash", "read"]; },
|
|
35
39
|
setActiveTools(_names: string[]) {},
|
|
36
40
|
};
|
|
@@ -161,6 +165,38 @@ test("a tool still works after a resume re-registers it against a fresh connecti
|
|
|
161
165
|
await fire("session_shutdown");
|
|
162
166
|
});
|
|
163
167
|
|
|
168
|
+
test("a reload does not self-collide: the second pass re-registers the bare name, dropping nothing", async () => {
|
|
169
|
+
// The reload self-collision: pi.getAllTools() returns this extension's own
|
|
170
|
+
// prior registration (no working unregisterTool in 0.84.4), so a naive
|
|
171
|
+
// builtins baseline mistakes the leaked fake_status for a builtin, prefixes
|
|
172
|
+
// to fake_fake_status, and after enough reloads even the prefixed name is
|
|
173
|
+
// taken and the tool is dropped — with a misleading "collides with a builtin"
|
|
174
|
+
// reason. The fix excludes the extension's own names from the baseline.
|
|
175
|
+
const { pi, fire, tools, registered } = fakePi();
|
|
176
|
+
createExtension(pi as never, {
|
|
177
|
+
env: {},
|
|
178
|
+
loadConfig: () => ({ fake: { command: process.execPath, args: [FAKE] } }),
|
|
179
|
+
});
|
|
180
|
+
await fire("session_start", { reason: "startup" });
|
|
181
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
182
|
+
// Two more reloads: without the fix, getAllTools() now reports fake_status,
|
|
183
|
+
// so the second pass prefixes and the third drops the prefixed name too.
|
|
184
|
+
await fire("session_start", { reason: "resume" });
|
|
185
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
186
|
+
await fire("session_start", { reason: "resume" });
|
|
187
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
188
|
+
assert.ok(tools.has("fake_status"), "the bare name must survive every reload");
|
|
189
|
+
assert.ok(!tools.has("fake_fake_status"), "the extension must never prefix against its own prior registration");
|
|
190
|
+
assert.ok(
|
|
191
|
+
!registered.includes("fake_fake_status"),
|
|
192
|
+
`no reload may register a self-prefixed name; registered: ${registered.join(", ")}`,
|
|
193
|
+
);
|
|
194
|
+
const tool = tools.get("fake_status");
|
|
195
|
+
const result = (await tool!.execute("call-1", {})) as { content: Array<{ text: string }> };
|
|
196
|
+
assert.deepEqual(result.content, [{ type: "text", text: "fake ok" }], "the bare tool must still work after reloads");
|
|
197
|
+
await fire("session_shutdown");
|
|
198
|
+
});
|
|
199
|
+
|
|
164
200
|
test("a tool result with isError:true is signaled by throwing, not returned silently", async () => {
|
|
165
201
|
const { pi, fire, tools } = fakePi();
|
|
166
202
|
createExtension(pi as never, {
|
package/src/index.ts
CHANGED
|
@@ -77,6 +77,14 @@ export function createExtension(pi: any, deps: Deps = {}): void {
|
|
|
77
77
|
}
|
|
78
78
|
| undefined;
|
|
79
79
|
const registeredNames = new Set<string>();
|
|
80
|
+
// Every name this extension has EVER registered, never cleared. pi 0.84.4
|
|
81
|
+
// has no working unregisterTool, so getAllTools() keeps returning our own
|
|
82
|
+
// prior registrations across reload/resume/fork. Without excluding them from
|
|
83
|
+
// the builtins baseline below, registerTools mistakes its own leaked tool
|
|
84
|
+
// for a builtin, prefixes it, and eventually drops it ("collides with a
|
|
85
|
+
// builtin"). registerTool replaces by name, so re-registering the bare
|
|
86
|
+
// self-namespaced name every reload is correct.
|
|
87
|
+
const ownNames = new Set<string>();
|
|
80
88
|
|
|
81
89
|
function uiSetStatus(status: string): void {
|
|
82
90
|
ui?.setStatus?.("channels", status === "" ? undefined : status);
|
|
@@ -91,7 +99,9 @@ export function createExtension(pi: any, deps: Deps = {}): void {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
function registerTools(connections: Connection[]): void {
|
|
94
|
-
const builtins = (pi.getAllTools?.() ?? [])
|
|
102
|
+
const builtins = (pi.getAllTools?.() ?? [])
|
|
103
|
+
.map((t: { name: string }) => t.name)
|
|
104
|
+
.filter((name: string) => !ownNames.has(name));
|
|
95
105
|
const { resolved, dropped } = resolveToolNames(
|
|
96
106
|
connections.map((c) => ({ name: c.name, tools: c.tools })),
|
|
97
107
|
builtins,
|
|
@@ -143,6 +153,7 @@ export function createExtension(pi: any, deps: Deps = {}): void {
|
|
|
143
153
|
// built from `connections.map(...)` above.
|
|
144
154
|
const conn = connections.find((c) => c.name === entry.server)!;
|
|
145
155
|
const def = conn.tools.find((t) => t.name === entry.original);
|
|
156
|
+
ownNames.add(entry.registered);
|
|
146
157
|
registeredNames.add(entry.registered);
|
|
147
158
|
pi.registerTool({
|
|
148
159
|
name: entry.registered,
|