viber-channel 0.8.16 → 0.8.17
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/lib/agent_tools.ts +28 -4
- package/lib/bridge_tool_host.ts +14 -3
- package/lib/claude_tool_defs.ts +16 -2
- package/lib/instance.ts +10 -0
- package/lib/peers.ts +62 -7
- package/lib/spawn_reason.ts +23 -0
- package/package.json +1 -1
- package/viber-channel.ts +1 -1
package/lib/agent_tools.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* MCP tool-result shape. It does NOT hold state.
|
|
18
18
|
*/
|
|
19
19
|
import { postMessage, parseArtifact, type Artifact } from "./messages.js";
|
|
20
|
-
import { listPeersAuto, openDm, type OpenDmResult } from "./peers.js";
|
|
20
|
+
import { listPeersAuto, openDm, type OpenDmResult, type PeerFilters } from "./peers.js";
|
|
21
21
|
import { ConversationTokenExpiredError } from "./messages.js";
|
|
22
22
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
23
23
|
|
|
@@ -98,12 +98,27 @@ function errorText(s: string): AgentToolResult {
|
|
|
98
98
|
* entry then carries a `project` field); a regular instance transparently
|
|
99
99
|
* falls back to its own project (unchanged behaviour, no `project` field).
|
|
100
100
|
*/
|
|
101
|
-
export async function listAgents(
|
|
101
|
+
export async function listAgents(
|
|
102
|
+
ctx: AgentToolsContext,
|
|
103
|
+
args: Record<string, unknown> = {},
|
|
104
|
+
): Promise<AgentToolResult> {
|
|
102
105
|
if (!ctx.instanceToken()) {
|
|
103
106
|
return errorText("Channel not ready: no instance identity yet.");
|
|
104
107
|
}
|
|
108
|
+
// #501 step-05 — optional filters, applied SERVER-side. Without them a
|
|
109
|
+
// dev-lead looking for its two reviewers pulls every agent the project ever
|
|
110
|
+
// created (30 rows for 5 live ones, measured), in a loop.
|
|
111
|
+
const filters: PeerFilters = {};
|
|
112
|
+
if (args.online === true) filters.online = true;
|
|
113
|
+
if (typeof args.label_prefix === "string" && args.label_prefix !== "") {
|
|
114
|
+
filters.labelPrefix = args.label_prefix;
|
|
115
|
+
}
|
|
105
116
|
try {
|
|
106
|
-
const { peers, scope } = await listPeersAuto(
|
|
117
|
+
const { peers, scope, presenceStatus } = await listPeersAuto(
|
|
118
|
+
ctx.baseUrl(),
|
|
119
|
+
ctx.instanceToken(),
|
|
120
|
+
filters,
|
|
121
|
+
);
|
|
107
122
|
// Project only what the model needs to pick a peer (drop last_seen /
|
|
108
123
|
// active_conversation_id — available over the wire if a future tool needs them).
|
|
109
124
|
const summary = peers.map((p) => ({
|
|
@@ -114,13 +129,22 @@ export async function listAgents(ctx: AgentToolsContext): Promise<AgentToolResul
|
|
|
114
129
|
// Present only in the user-scoped (orchestrator) listing.
|
|
115
130
|
...(p.project_name !== undefined ? { project: p.project_name } : {}),
|
|
116
131
|
}));
|
|
132
|
+
// An `online` filter under an unreadable presence source would come back
|
|
133
|
+
// empty and read as "nobody is alive" — the false green in listing form.
|
|
134
|
+
// The server drops the filter in that case; say so rather than let the
|
|
135
|
+
// caller believe it was applied.
|
|
136
|
+
const presenceWarning =
|
|
137
|
+
filters.online === true && presenceStatus === "unavailable"
|
|
138
|
+
? "WARNING: the presence source is unavailable, so the `online` filter was NOT applied — " +
|
|
139
|
+
"`online` values below are unknown, not observed.\n\n"
|
|
140
|
+
: "";
|
|
117
141
|
const body =
|
|
118
142
|
summary.length === 0
|
|
119
143
|
? scope === "user"
|
|
120
144
|
? "No other agents are currently registered in any of your projects."
|
|
121
145
|
: "No other agents are currently registered in this project."
|
|
122
146
|
: JSON.stringify(summary, null, 2);
|
|
123
|
-
return text(body);
|
|
147
|
+
return text(`${presenceWarning}${body}`);
|
|
124
148
|
} catch (err) {
|
|
125
149
|
return errorText(`list_agents failed: ${String(err)}`);
|
|
126
150
|
}
|
package/lib/bridge_tool_host.ts
CHANGED
|
@@ -92,8 +92,19 @@ export const TOOL_DEFS = [
|
|
|
92
92
|
"List the OTHER agents (instances) you can DM. Returns each agent's id, " +
|
|
93
93
|
"label, runtime kind, and whether it is online. Use it to find an id before message_agent. " +
|
|
94
94
|
"Normally project-scoped; an ORCHESTRATOR instance (#307) sees all the owner's projects, " +
|
|
95
|
-
"each entry tagged with a `project` field."
|
|
96
|
-
|
|
95
|
+
"each entry tagged with a `project` field. " +
|
|
96
|
+
"FILTER instead of listing everything (#501): `online: true` for live agents only, " +
|
|
97
|
+
"`label_prefix` for one team (e.g. \"501-\"). Both are applied server-side. " +
|
|
98
|
+
"If the presence source is unreachable the `online` filter is DROPPED and the answer " +
|
|
99
|
+
"says so — an empty list would wrongly read as \"no agent is alive\".",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
online: { type: "boolean", description: "Only agents currently seen online." },
|
|
104
|
+
label_prefix: { type: "string", description: 'Only labels starting with this, e.g. "501-".' },
|
|
105
|
+
},
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
97
108
|
},
|
|
98
109
|
{
|
|
99
110
|
name: "message_agent",
|
|
@@ -146,7 +157,7 @@ export async function dispatchBridgeTool(
|
|
|
146
157
|
): Promise<AgentToolResult> {
|
|
147
158
|
let result: AgentToolResult;
|
|
148
159
|
if (name === "list_agents") {
|
|
149
|
-
result = await listAgents(opts.ctx);
|
|
160
|
+
result = await listAgents(opts.ctx, args);
|
|
150
161
|
} else if (name === "message_agent") {
|
|
151
162
|
result = await messageAgent(opts.ctx, args);
|
|
152
163
|
} else if (name === "send_message") {
|
package/lib/claude_tool_defs.ts
CHANGED
|
@@ -71,10 +71,24 @@ export const CLAUDE_TOOL_DEFS = [
|
|
|
71
71
|
"Use this to find the id of an agent (e.g. 'Codex Review') before calling message_agent. " +
|
|
72
72
|
"Normally scoped to this project; if the owner designated this instance an ORCHESTRATOR " +
|
|
73
73
|
"(#307), the list covers ALL the owner's projects and each entry carries a `project` field. " +
|
|
74
|
-
"You are never in the list."
|
|
74
|
+
"You are never in the list. " +
|
|
75
|
+
"FILTER instead of listing everything (#501): `online: true` returns only agents the server " +
|
|
76
|
+
'currently sees online, `label_prefix` only those whose label starts with it (e.g. "501-" ' +
|
|
77
|
+
"for one team). Both are applied server-side, so an unfiltered call in a loop is the " +
|
|
78
|
+
"expensive path. If the presence source is unreachable the `online` filter is DROPPED and " +
|
|
79
|
+
'the answer says so — an empty list would wrongly read as "no agent is alive".',
|
|
75
80
|
inputSchema: {
|
|
76
81
|
type: "object" as const,
|
|
77
|
-
properties: {
|
|
82
|
+
properties: {
|
|
83
|
+
online: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description: "Only agents the server currently sees online.",
|
|
86
|
+
},
|
|
87
|
+
label_prefix: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: 'Only agents whose label starts with this prefix, e.g. "501-".',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
78
92
|
required: [],
|
|
79
93
|
additionalProperties: false,
|
|
80
94
|
},
|
package/lib/instance.ts
CHANGED
|
@@ -193,6 +193,16 @@ export interface AcquiredInstance {
|
|
|
193
193
|
* 3. `auth.json` `instance_token` — reused (durable identity, D4 → same id).
|
|
194
194
|
* 4. register a NEW instance with the stable project_token, persisted back to
|
|
195
195
|
* auth.json (self-healing migration — no forced manual reconnect).
|
|
196
|
+
*
|
|
197
|
+
* ⚠ #501 DEPENDS ON THE PRIORITY ORDER ABOVE. vibe-master's readiness gate proves
|
|
198
|
+
* an agent came up by watching for an instance id that did NOT exist before the
|
|
199
|
+
* launch. Only route 2 mints one: routes 1 and 3 REUSE an id, so a launch that
|
|
200
|
+
* took either would be reported as "never registered" while being perfectly
|
|
201
|
+
* healthy — a false red. vibe-master's spawns take route 2 because
|
|
202
|
+
* `vibe-master/lib/terminal.ts` scrubs `VIBER_INSTANCE_TOKEN`/`_ID` from the
|
|
203
|
+
* child environment; the guarantee lives there, and this comment exists so a
|
|
204
|
+
* change here is not made unaware of it. Locked by the route-precedence tests in
|
|
205
|
+
* `test/instance.test.ts`.
|
|
196
206
|
*/
|
|
197
207
|
export async function acquireInstance(
|
|
198
208
|
baseUrl: string,
|
package/lib/peers.ts
CHANGED
|
@@ -44,12 +44,41 @@ export class PeersHttpError extends Error {
|
|
|
44
44
|
* Throws PeersHttpError on a non-2xx response so the caller can surface a
|
|
45
45
|
* clear tool error — or fall back from user scope on a 403.
|
|
46
46
|
*/
|
|
47
|
+
/** A listing plus whether the presence source could be read at all (#501). */
|
|
48
|
+
export interface PeersListing {
|
|
49
|
+
peers: PeerView[];
|
|
50
|
+
presenceStatus: "available" | "unavailable";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PeerFilters {
|
|
54
|
+
/** Only agents the server currently sees online. Dropped, server-side, when
|
|
55
|
+
* presence is unavailable — an empty list would read as "nobody is alive". */
|
|
56
|
+
online?: boolean;
|
|
57
|
+
/** Team/label prefix, e.g. "501-". */
|
|
58
|
+
labelPrefix?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
export async function listPeers(
|
|
48
62
|
baseUrl: string,
|
|
49
63
|
instanceToken: string,
|
|
50
64
|
scope?: "user",
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
filters: PeerFilters = {},
|
|
66
|
+
): Promise<PeersListing> {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (scope === "user") params.set("scope", "user");
|
|
69
|
+
// #501 step-05: filtering happens SERVER-side. Doing it here would still pay
|
|
70
|
+
// the transfer that made this tool expensive to call in a loop.
|
|
71
|
+
if (filters.online === true) params.set("online", "true");
|
|
72
|
+
if (filters.labelPrefix !== undefined && filters.labelPrefix !== "") {
|
|
73
|
+
params.set("label_prefix", filters.labelPrefix);
|
|
74
|
+
}
|
|
75
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
76
|
+
// ⚠ NEVER add `X-Client-Fingerprint` to these headers (#501). The server picks
|
|
77
|
+
// its auth path from that header's PRESENCE: with it, `/peers` validates the
|
|
78
|
+
// Bearer as a project_token instead of an instance_token — and there is no
|
|
79
|
+
// fallback between the two, by design. Adding it here (directly, via a fetch
|
|
80
|
+
// wrapper, or via a proxy) would 401 `list_agents` for the whole fleet, with a
|
|
81
|
+
// cause invisible from this file.
|
|
53
82
|
const resp = await fetch(`${baseUrl}/api/instances/peers${qs}`, {
|
|
54
83
|
method: "GET",
|
|
55
84
|
headers: { Authorization: `Bearer ${instanceToken}`, ...cfAccessHeaders() },
|
|
@@ -64,8 +93,27 @@ export async function listPeers(
|
|
|
64
93
|
}
|
|
65
94
|
throw new PeersHttpError(`listPeers failed: ${detail}`, resp.status);
|
|
66
95
|
}
|
|
67
|
-
const body = (await resp.json()) as {
|
|
68
|
-
|
|
96
|
+
const body = (await resp.json()) as {
|
|
97
|
+
instances?: PeerView[];
|
|
98
|
+
presence_status?: string;
|
|
99
|
+
};
|
|
100
|
+
// A body we cannot read is a READ FAILURE, not an empty project (#501). The
|
|
101
|
+
// old `?? []` turned `{"presence_status":"available"}` into "No other agents
|
|
102
|
+
// are registered" — the exact false conclusion this work exists to remove.
|
|
103
|
+
// The same rule already guards the readiness client and the server's presence
|
|
104
|
+
// snapshot; it applies here too.
|
|
105
|
+
if (!Array.isArray(body.instances)) {
|
|
106
|
+
throw new PeersHttpError(
|
|
107
|
+
`listPeers: unreadable body (no \`instances\` array) — refusing to report an empty project`,
|
|
108
|
+
resp.status,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
peers: body.instances,
|
|
113
|
+
// Only the literal "available" is trusted: an older backend that does not
|
|
114
|
+
// send the field must fall on the cautious side (#501).
|
|
115
|
+
presenceStatus: body.presence_status === "available" ? "available" : "unavailable",
|
|
116
|
+
};
|
|
69
117
|
}
|
|
70
118
|
|
|
71
119
|
/**
|
|
@@ -78,12 +126,19 @@ export async function listPeers(
|
|
|
78
126
|
export async function listPeersAuto(
|
|
79
127
|
baseUrl: string,
|
|
80
128
|
instanceToken: string,
|
|
81
|
-
|
|
129
|
+
filters: PeerFilters = {},
|
|
130
|
+
): Promise<PeersListing & { scope: "user" | "project" }> {
|
|
82
131
|
try {
|
|
83
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
...(await listPeers(baseUrl, instanceToken, "user", filters)),
|
|
134
|
+
scope: "user",
|
|
135
|
+
};
|
|
84
136
|
} catch (err) {
|
|
85
137
|
if (err instanceof PeersHttpError && err.status === 403) {
|
|
86
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
...(await listPeers(baseUrl, instanceToken, undefined, filters)),
|
|
140
|
+
scope: "project",
|
|
141
|
+
};
|
|
87
142
|
}
|
|
88
143
|
throw err;
|
|
89
144
|
}
|
package/lib/spawn_reason.ts
CHANGED
|
@@ -25,6 +25,9 @@ export const SPAWN_REASONS = [
|
|
|
25
25
|
"roster_invalid",
|
|
26
26
|
"codex_unavailable",
|
|
27
27
|
"launch_failed",
|
|
28
|
+
"not_ready",
|
|
29
|
+
"readiness_unverified",
|
|
30
|
+
"launch_refused",
|
|
28
31
|
// Emitted by the runner itself
|
|
29
32
|
"policy_invalid",
|
|
30
33
|
"policy_refused",
|
|
@@ -224,6 +227,26 @@ export function messageForReason(
|
|
|
224
227
|
return "codex is not launchable on this machine, so no agent was started — install codex or set VIBER_CODEX_BIN";
|
|
225
228
|
case "launch_failed":
|
|
226
229
|
return `a member failed to start${names}; the rest of the roster was skipped — the team is incomplete`;
|
|
230
|
+
// #501 — these two used to borrow `launch_failed`, whose sentence says the
|
|
231
|
+
// agent never started AND that the roster was skipped. For an agent that is
|
|
232
|
+
// running, and a roster that Q1bis deliberately did NOT interrupt, that
|
|
233
|
+
// sentence is false twice — and an automated caller retrying on it would
|
|
234
|
+
// create the duplicate labels we just built a detector for.
|
|
235
|
+
case "not_ready":
|
|
236
|
+
// Deliberately NOT "not registered": this family also covers an agent that
|
|
237
|
+
// registered and never beat, one whose process died after registering, and
|
|
238
|
+
// one whose instance vanished. Naming only the first would deny a
|
|
239
|
+
// registration the server can prove (Codex review). The precise reason
|
|
240
|
+
// stays in the structured human recap.
|
|
241
|
+
return `these agents STARTED but did not become ready on the channel${names} — the rest of the roster was NOT interrupted; close their window (terminal agents are not stopped automatically) and spawn them again`;
|
|
242
|
+
case "launch_refused":
|
|
243
|
+
// Says neither "started" nor "skipped": both would be false here.
|
|
244
|
+
return `no process was started for these members${names} — another launch of the same label was already in flight on this machine, so the spawn was refused locally; the rest of the roster was NOT interrupted. Re-spawn them with a distinct prefix`;
|
|
245
|
+
case "readiness_unverified":
|
|
246
|
+
// Also covers post-launch correlation ambiguity, not just an unreadable
|
|
247
|
+
// presence source — so the sentence states the CONSEQUENCE (we could not
|
|
248
|
+
// establish it) rather than one of its causes.
|
|
249
|
+
return `these agents started, but their availability could NOT be established${names} — this says nothing against them. Nothing was stopped; check them with list_agents before relying on them`;
|
|
227
250
|
case "policy_invalid":
|
|
228
251
|
return "this machine's local runner policy is present but invalid, so the spawn was refused";
|
|
229
252
|
case "policy_refused":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viber-channel",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
4
4
|
"description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/viber-channel.ts
CHANGED
|
@@ -475,7 +475,7 @@ async function handleCallTool(
|
|
|
475
475
|
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
|
|
476
476
|
|
|
477
477
|
if (request.params.name === "list_agents") {
|
|
478
|
-
return libListAgents(channelToolsCtx);
|
|
478
|
+
return libListAgents(channelToolsCtx, args);
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
if (request.params.name === "message_agent") {
|