viberoom 0.5.9 → 0.6.0
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/README.md +2 -0
- package/dist/agent-health.js +52 -0
- package/dist/duration.js +12 -0
- package/dist/hub.js +9 -0
- package/dist/launcher.js +19 -2
- package/dist/main.js +77 -34
- package/dist/mcp-skills-server.js +24 -0
- package/dist/persona.js +69 -14
- package/dist/quotes.js +41 -0
- package/dist/recipes.js +28 -1
- package/dist/room.js +136 -33
- package/dist/rows.js +19 -0
- package/dist/server.js +251 -27
- package/dist/update.js +32 -2
- package/dist/viewer.js +69 -2
- package/dist/ws.js +175 -0
- package/package.json +2 -1
- package/ui/app.css +341 -251
- package/ui/app.js +1283 -278
- package/ui/avatars.js +6 -4
- package/ui/components.css +140 -0
- package/ui/components.js +342 -0
- package/ui/icons.js +9 -0
- package/ui/index.html +58 -36
- package/ui/theme.css +450 -183
- package/ui/tokens.js +620 -0
- package/ui/ui.js +113 -0
package/dist/recipes.js
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { loginState } from "./agent-health.js";
|
|
8
9
|
const isWindows = process.platform === "win32";
|
|
9
10
|
function resolvePackageEntry(packageName, relativeEntry) {
|
|
10
11
|
try {
|
|
@@ -167,6 +168,9 @@ const recipes = [
|
|
|
167
168
|
unavailableReason: claudeExe ? null : "Claude Code not found",
|
|
168
169
|
installedAt: claudeExe,
|
|
169
170
|
installHint: "install Claude Code (npm install -g @anthropic-ai/claude-code, or the native installer) and log in with `claude`",
|
|
171
|
+
loginCommand: "claude",
|
|
172
|
+
login: { env: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"], files: [".claude/.credentials.json"], command: "claude", fileless: ["darwin"] },
|
|
173
|
+
loginState: "unknown",
|
|
170
174
|
build: ({ model }) => ({
|
|
171
175
|
command: process.execPath,
|
|
172
176
|
args: [claudeAdapter],
|
|
@@ -190,6 +194,9 @@ const recipes = [
|
|
|
190
194
|
unavailableReason: codexExe ? null : "Codex CLI not found",
|
|
191
195
|
installedAt: codexExe,
|
|
192
196
|
installHint: "install Codex (npm install -g @openai/codex) and log in with `codex login`",
|
|
197
|
+
loginCommand: "codex login",
|
|
198
|
+
login: { env: ["CODEX_API_KEY", "OPENAI_API_KEY"], files: [".codex/auth.json"], command: "codex login" },
|
|
199
|
+
loginState: "unknown",
|
|
193
200
|
build: () => ({
|
|
194
201
|
command: process.execPath,
|
|
195
202
|
args: [codexAdapter],
|
|
@@ -213,6 +220,9 @@ const recipes = [
|
|
|
213
220
|
unavailableReason: geminiEntry ? null : "Gemini CLI not found",
|
|
214
221
|
installedAt: geminiEntry,
|
|
215
222
|
installHint: "npm install -g @google/gemini-cli, then sign in once (gemini)",
|
|
223
|
+
loginCommand: "gemini",
|
|
224
|
+
login: { env: ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_APPLICATION_CREDENTIALS"], files: [".gemini/google_accounts.json", ".gemini/oauth_creds.json"], command: "gemini" },
|
|
225
|
+
loginState: "unknown",
|
|
216
226
|
modelAtLaunch: true,
|
|
217
227
|
build: ({ model }) => ({
|
|
218
228
|
command: process.execPath,
|
|
@@ -237,6 +247,9 @@ const recipes = [
|
|
|
237
247
|
unavailableReason: cursorAgent ? null : "Cursor CLI not found",
|
|
238
248
|
installedAt: cursorAgent?.index ?? null,
|
|
239
249
|
installHint: "install cursor-agent (cursor.com/cli), then agent login",
|
|
250
|
+
loginCommand: "cursor-agent login",
|
|
251
|
+
login: { env: ["CURSOR_API_KEY"], files: [], command: "cursor-agent login" },
|
|
252
|
+
loginState: "unknown",
|
|
240
253
|
build: () => ({
|
|
241
254
|
command: cursorAgent?.node ?? "",
|
|
242
255
|
args: [cursorAgent?.index ?? "", "acp"],
|
|
@@ -260,6 +273,9 @@ const recipes = [
|
|
|
260
273
|
unavailableReason: openCodeExe ? null : "OpenCode not found",
|
|
261
274
|
installedAt: openCodeExe,
|
|
262
275
|
installHint: "npm install -g opencode-ai (or curl -fsSL https://opencode.ai/install | bash), then opencode providers",
|
|
276
|
+
loginCommand: "opencode auth login",
|
|
277
|
+
login: { env: [], files: [".local/share/opencode/auth.json", "AppData/Local/opencode/auth.json", ".config/opencode/auth.json"], command: "opencode auth login" },
|
|
278
|
+
loginState: "unknown",
|
|
263
279
|
build: () => ({
|
|
264
280
|
command: openCodeExe ?? "",
|
|
265
281
|
args: ["acp"],
|
|
@@ -286,6 +302,9 @@ const recipes = [
|
|
|
286
302
|
unavailableReason: copilotExe ? null : "GitHub Copilot CLI not found",
|
|
287
303
|
installedAt: copilotExe,
|
|
288
304
|
installHint: "winget install GitHub.Copilot / brew install copilot-cli / npm install -g @github/copilot, then copilot login",
|
|
305
|
+
loginCommand: "copilot",
|
|
306
|
+
login: { env: ["GITHUB_TOKEN", "GH_TOKEN", "COPILOT_API_KEY"], files: [], command: "copilot" },
|
|
307
|
+
loginState: "unknown",
|
|
289
308
|
modelAtLaunch: true,
|
|
290
309
|
build: ({ model }) => ({
|
|
291
310
|
command: copilotExe ?? "",
|
|
@@ -311,13 +330,21 @@ if (fakeAgent) {
|
|
|
311
330
|
unavailableReason: null,
|
|
312
331
|
installedAt: fakeAgent,
|
|
313
332
|
installHint: "",
|
|
333
|
+
loginCommand: "",
|
|
334
|
+
loginState: "ok",
|
|
314
335
|
bypassMode: null,
|
|
315
336
|
build: () => ({ command: process.execPath, args: [fakeAgent], env: {} }),
|
|
316
337
|
});
|
|
317
338
|
}
|
|
339
|
+
function loginEvidence() {
|
|
340
|
+
return { env: process.env, platform: process.platform, exists: (relative) => existsSync(join(homedir(), ...relative.split("/"))) };
|
|
341
|
+
}
|
|
318
342
|
export function listRecipes() {
|
|
343
|
+
const evidence = loginEvidence();
|
|
344
|
+
for (const recipe of recipes)
|
|
345
|
+
recipe.loginState = recipe.unavailableReason ? "unknown" : loginState(recipe.login, evidence);
|
|
319
346
|
return recipes;
|
|
320
347
|
}
|
|
321
348
|
export function getRecipe(id) {
|
|
322
|
-
return
|
|
349
|
+
return listRecipes().find((r) => r.id === id);
|
|
323
350
|
}
|
package/dist/room.js
CHANGED
|
@@ -4,12 +4,16 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.js";
|
|
6
6
|
import { NOTES_ONLY_PROMPT, NOTES_REQUEST, crossedThreshold, emptyUsageReport, extractNotes, isBareContextFullError, isContextFullError, looksCompacted, overThreshold, visibleChunk } from "./context.js";
|
|
7
|
+
import { formatDuration } from "./duration.js";
|
|
7
8
|
import { affectedByEdit, editNotice, partitionHistory, rewriteNotice } from "./edit.js";
|
|
8
9
|
import { saveImages } from "./files.js";
|
|
10
|
+
import { agentReadableWindow, resolveQuotes } from "./quotes.js";
|
|
9
11
|
import { join, resolve } from "node:path";
|
|
10
12
|
import { AcpAgent } from "./acp-client.js";
|
|
11
13
|
import { RemoteError } from "./jsonrpc.js";
|
|
12
14
|
import { getRecipe, listRecipes } from "./recipes.js";
|
|
15
|
+
import { classifyStartFailure } from "./agent-health.js";
|
|
16
|
+
import { legacyRowTone } from "./rows.js";
|
|
13
17
|
import { composeSkillBlock, skillPull, SKILL_TOOL_NAME } from "./persona.js";
|
|
14
18
|
import { BUILTIN_AUTHOR, parseSkillInvocation, renderSkillBody, SKILL_NAME_PATTERN, } from "./skills.js";
|
|
15
19
|
import { templateId } from "./templates.js";
|
|
@@ -93,6 +97,11 @@ export class Room extends EventEmitter {
|
|
|
93
97
|
for (const line of lines) {
|
|
94
98
|
try {
|
|
95
99
|
const message = JSON.parse(line);
|
|
100
|
+
if (message.kind === "system" && !message.details?.tone) {
|
|
101
|
+
const tone = legacyRowTone(message.text);
|
|
102
|
+
if (tone)
|
|
103
|
+
message.details = { ...(message.details ?? {}), tone };
|
|
104
|
+
}
|
|
96
105
|
this.messages.push(message);
|
|
97
106
|
if (message.seq > this.seq)
|
|
98
107
|
this.seq = message.seq;
|
|
@@ -212,6 +221,19 @@ export class Room extends EventEmitter {
|
|
|
212
221
|
appendFileSync(this.historyPath(), JSON.stringify(message) + "\n");
|
|
213
222
|
this.push({ type: "message", message });
|
|
214
223
|
}
|
|
224
|
+
messagesWithLiveDrafts() {
|
|
225
|
+
const live = [...this.runtimes.values()].filter((r) => r.turn?.published).map((r) => r.turn.message);
|
|
226
|
+
if (!live.length)
|
|
227
|
+
return [...this.messages];
|
|
228
|
+
const out = [...this.messages];
|
|
229
|
+
for (const draft of live) {
|
|
230
|
+
let at = out.length;
|
|
231
|
+
while (at > 0 && out[at - 1].ts > draft.ts)
|
|
232
|
+
at--;
|
|
233
|
+
out.splice(at, 0, draft);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
215
237
|
get humanName() {
|
|
216
238
|
return this.settings.humanName;
|
|
217
239
|
}
|
|
@@ -232,7 +254,7 @@ export class Room extends EventEmitter {
|
|
|
232
254
|
settings: this.settings,
|
|
233
255
|
customRulesText: this.renderRuleReferences(this.settings.customRules),
|
|
234
256
|
participants: [...this.participants.values()],
|
|
235
|
-
messages:
|
|
257
|
+
messages: this.messagesWithLiveDrafts(),
|
|
236
258
|
permissions: [...this.permissions.values()].map(({ resolve: _r, ...p }) => p),
|
|
237
259
|
proposals: [...this.proposals.values()],
|
|
238
260
|
recipes: listRecipes().map(({ build: _b, ...r }) => r),
|
|
@@ -296,13 +318,14 @@ export class Room extends EventEmitter {
|
|
|
296
318
|
unstaffed() {
|
|
297
319
|
return [...this.participants.values()].filter((p) => p.kind === "agent" && p.status === "unstaffed");
|
|
298
320
|
}
|
|
299
|
-
postHumanMessage(text, images = []) {
|
|
321
|
+
postHumanMessage(text, images = [], quotes = []) {
|
|
300
322
|
const waiting = this.unstaffed();
|
|
301
323
|
if (waiting.length)
|
|
302
324
|
throw new Error(`${waiting.map((p) => p.name).join(", ")} ${waiting.length === 1 ? "has" : "have"} no coding agent yet: summon ${waiting.length === 1 ? "it" : "them"} from the roster to start the conversation`);
|
|
303
325
|
const trimmed = text.trim();
|
|
304
|
-
if (!trimmed && !images.length)
|
|
326
|
+
if (!trimmed && !images.length && !quotes.length)
|
|
305
327
|
throw new Error("empty message");
|
|
328
|
+
const quoted = quotes.length ? resolveQuotes(quotes, this.messages) : [];
|
|
306
329
|
const attachments = images.length ? saveImages(ensureDir(this.filesDir()), images) : [];
|
|
307
330
|
this.humanTypingUntil = 0;
|
|
308
331
|
const human = this.participants.get("human");
|
|
@@ -319,6 +342,8 @@ export class Room extends EventEmitter {
|
|
|
319
342
|
};
|
|
320
343
|
if (attachments.length)
|
|
321
344
|
message.images = attachments;
|
|
345
|
+
if (quoted.length)
|
|
346
|
+
message.quotes = quoted;
|
|
322
347
|
this.decorateHumanMessage(message);
|
|
323
348
|
human.turns += 1;
|
|
324
349
|
if (this.focused) {
|
|
@@ -470,6 +495,7 @@ export class Room extends EventEmitter {
|
|
|
470
495
|
const memory = !!options.memory;
|
|
471
496
|
const replay = memory ? Math.max(0, options.replay ?? this.settings.replayAfterRestart) : 0;
|
|
472
497
|
const withNotes = memory && !!participant.notes;
|
|
498
|
+
const why = options.reason ?? "its context was cleared";
|
|
473
499
|
this.dropScheduledTurn(id);
|
|
474
500
|
this.cancelPermissionsOf(id);
|
|
475
501
|
if (this.speaking === id)
|
|
@@ -480,18 +506,38 @@ export class Room extends EventEmitter {
|
|
|
480
506
|
this.restoredSeen.set(id, this.seq);
|
|
481
507
|
this.push({ type: "participant", participant });
|
|
482
508
|
if (!online) {
|
|
483
|
-
participant.statusDetail = withNotes ?
|
|
484
|
-
|
|
509
|
+
participant.statusDetail = withNotes ? `${why}; a reconnect starts it with its notes` : `${why}; a reconnect starts it with an empty head`;
|
|
510
|
+
const comesBack = withNotes ? "it comes back with its notes" : "it comes back knowing nothing from before";
|
|
511
|
+
this.postSystem(options.reason ? `${participant.name} was respawned while offline (${why}): ${comesBack}.` : `${participant.name} was respawned while offline: ${comesBack}.`);
|
|
485
512
|
this.push({ type: "participant", participant });
|
|
486
513
|
this.log.info(`respawn of ${participant.name} (offline): stored session dropped`);
|
|
487
514
|
return participant;
|
|
488
515
|
}
|
|
489
516
|
await this.reconnect(id, memory
|
|
490
|
-
? { mode: "replay", replay, memory: withNotes, reason:
|
|
491
|
-
: { mode: "replay", replay: 0, reason:
|
|
517
|
+
? { mode: "replay", replay, memory: withNotes, reason: `${why}; it comes back with ${withNotes ? "its notes and " : ""}the last ${replay} messages` }
|
|
518
|
+
: { mode: "replay", replay: 0, reason: `${why}, it remembers nothing from before` });
|
|
492
519
|
this.log.info(`respawn of ${participant.name}: fresh session, ${memory ? `${withNotes ? "notes + " : ""}replay ${replay}` : "no replay"}`);
|
|
493
520
|
return participant;
|
|
494
521
|
}
|
|
522
|
+
async restartWithPersona(id, patch) {
|
|
523
|
+
const participant = this.participants.get(id);
|
|
524
|
+
if (!participant || participant.kind !== "agent")
|
|
525
|
+
throw new Error("no such agent");
|
|
526
|
+
const runtime = this.runtimes.get(id);
|
|
527
|
+
const online = !!runtime && runtime.agent.alive;
|
|
528
|
+
if (online && runtime.turnActive)
|
|
529
|
+
throw new Error(`${participant.name} is in the middle of a reply; try again when it is idle`);
|
|
530
|
+
if (online) {
|
|
531
|
+
try {
|
|
532
|
+
await this.takeNotes(id);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
this.log.warn(`${participant.name}: notes before the restart failed (${describeError(error)}); it restarts with the notes it had`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
this.updatePersona(id, patch);
|
|
539
|
+
return this.respawnAgent(id, { memory: true, reason: "its role changed" });
|
|
540
|
+
}
|
|
495
541
|
updateNotes(id, notes) {
|
|
496
542
|
const participant = this.participants.get(id);
|
|
497
543
|
if (!participant || participant.kind !== "agent")
|
|
@@ -514,7 +560,15 @@ export class Room extends EventEmitter {
|
|
|
514
560
|
throw new Error(`${participant.name} is in the middle of a reply; try again when it is idle`);
|
|
515
561
|
const header = buildHeader(this.effectiveSettings(), this.personaOf(participant), this.roster(), this.hops, ["hidden turn: notes only, nothing is posted"]);
|
|
516
562
|
runtime.log.info("notes: hidden turn");
|
|
517
|
-
|
|
563
|
+
participant.notesTurn = true;
|
|
564
|
+
this.push({ type: "participant", participant });
|
|
565
|
+
try {
|
|
566
|
+
await this.executeTurn(participant, runtime, [{ type: "text", text: `${header}\n\n${NOTES_ONLY_PROMPT}` }], null, true);
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
participant.notesTurn = undefined;
|
|
570
|
+
this.push({ type: "participant", participant });
|
|
571
|
+
}
|
|
518
572
|
return participant;
|
|
519
573
|
}
|
|
520
574
|
keepNotes(participant, runtime, notes, via) {
|
|
@@ -536,14 +590,14 @@ export class Room extends EventEmitter {
|
|
|
536
590
|
participant.status = "error";
|
|
537
591
|
participant.statusDetail = "its context filled up again right after a respawn; it needs you (respawn it by hand, with fewer replayed messages)";
|
|
538
592
|
this.push({ type: "participant", participant });
|
|
539
|
-
this.postSystem(`${participant.name} ran out of context again (${detail.slice(0, 120)}); it was respawned once already and now needs you.`, "human");
|
|
593
|
+
this.postSystem(`${participant.name} ran out of context again (${detail.slice(0, 120)}); it was respawned once already and now needs you.`, "human", false, { tone: "error" });
|
|
540
594
|
runtime.log.warn(`context full again within 10 minutes: no automatic respawn`);
|
|
541
595
|
return;
|
|
542
596
|
}
|
|
543
597
|
participant.autoRespawnAt = Date.now();
|
|
544
598
|
this.push({ type: "participant", participant });
|
|
545
599
|
const memory = !!participant.notes;
|
|
546
|
-
this.postSystem(`${participant.name} ran out of context (${detail.slice(0, 120)}); it is respawned ${memory ? `with its notes and the last ${this.settings.replayAfterRestart} messages` : `with the last ${this.settings.replayAfterRestart} messages (it had no notes)`}.`, "human");
|
|
600
|
+
this.postSystem(`${participant.name} ran out of context (${detail.slice(0, 120)}); it is respawned ${memory ? `with its notes and the last ${this.settings.replayAfterRestart} messages` : `with the last ${this.settings.replayAfterRestart} messages (it had no notes)`}.`, "human", false, { tone: "error" });
|
|
547
601
|
runtime.log.warn(`context full: ${detail}; respawn with ${memory ? "notes" : "no notes"}`);
|
|
548
602
|
setImmediate(() => {
|
|
549
603
|
void (memory ? this.respawnAgent(participant.id, { memory: true }) : this.reconnectAfterFull(participant.id)).catch((error) => this.notice(`${participant.name}: respawn after a full context failed: ${describeError(error)}`, "error"));
|
|
@@ -599,7 +653,7 @@ export class Room extends EventEmitter {
|
|
|
599
653
|
}
|
|
600
654
|
this.focused = true;
|
|
601
655
|
this.push(this.roomEvent());
|
|
602
|
-
this.postSystem(`Hush: ${stopped ? `${stopped} repl${stopped > 1 ? "ies" : "y"} stopped; ` : ""}everyone waits until ${this.humanName} writes again
|
|
656
|
+
this.postSystem(`Hush: ${stopped ? `${stopped} repl${stopped > 1 ? "ies" : "y"} stopped; ` : ""}everyone waits until ${this.humanName} writes again.`, undefined, false, { tone: "hush" });
|
|
603
657
|
}
|
|
604
658
|
rename(name) {
|
|
605
659
|
const trimmed = name.trim();
|
|
@@ -887,20 +941,25 @@ export class Room extends EventEmitter {
|
|
|
887
941
|
const spec = recipe.build({ model: launch.model });
|
|
888
942
|
const transcript = new Transcript(join(this.dataDir, "transcripts"), name);
|
|
889
943
|
log.info(`spawning ${spec.command} ${spec.args.join(" ")} (cwd ${cwd}); transcript ${transcript.path}`);
|
|
944
|
+
const stderrTail = [];
|
|
890
945
|
let agent;
|
|
891
946
|
try {
|
|
892
947
|
agent = new AcpAgent({ ...spec, cwd }, {
|
|
893
948
|
onSessionUpdate: (_sessionId, update) => this.onSessionUpdate(id, update),
|
|
894
949
|
onPermissionRequest: (params) => this.onPermissionRequest(id, params),
|
|
895
|
-
onStderr: (line) =>
|
|
950
|
+
onStderr: (line) => {
|
|
951
|
+
log.info(`stderr: ${line}`);
|
|
952
|
+
stderrTail.push(line);
|
|
953
|
+
if (stderrTail.length > 10)
|
|
954
|
+
stderrTail.shift();
|
|
955
|
+
},
|
|
896
956
|
onExit: (code, signal) => this.onAgentExit(id, code, signal, agent),
|
|
897
957
|
onRaw: (direction, message) => transcript.record(direction, message),
|
|
898
958
|
onProtocolError: (text) => log.warn(`protocol: ${text}`),
|
|
899
959
|
});
|
|
900
960
|
}
|
|
901
961
|
catch (error) {
|
|
902
|
-
this.failStart(participant, error, fresh);
|
|
903
|
-
throw error;
|
|
962
|
+
throw this.failStart(participant, error, fresh, stderrTail);
|
|
904
963
|
}
|
|
905
964
|
try {
|
|
906
965
|
const init = await agent.initialize({ name: "viberoom", version: "0.2.0" });
|
|
@@ -993,6 +1052,7 @@ export class Room extends EventEmitter {
|
|
|
993
1052
|
this.notice(`${name}: ${w}`, "warn");
|
|
994
1053
|
participant.status = "idle";
|
|
995
1054
|
participant.statusDetail = undefined;
|
|
1055
|
+
participant.trouble = undefined;
|
|
996
1056
|
this.push({ type: "participant", participant });
|
|
997
1057
|
if (fresh) {
|
|
998
1058
|
const detail = this.settings.showVendorInRoster ? `${recipe.label}${participant.model ? `, model ${participant.model}` : ""}` : "agent";
|
|
@@ -1024,8 +1084,7 @@ export class Room extends EventEmitter {
|
|
|
1024
1084
|
catch (error) {
|
|
1025
1085
|
agent.kill();
|
|
1026
1086
|
this.forgetRuntime(id);
|
|
1027
|
-
this.failStart(participant, error, fresh);
|
|
1028
|
-
throw error;
|
|
1087
|
+
throw this.failStart(participant, error, fresh, stderrTail);
|
|
1029
1088
|
}
|
|
1030
1089
|
}
|
|
1031
1090
|
async removeParticipant(id) {
|
|
@@ -1069,11 +1128,20 @@ export class Room extends EventEmitter {
|
|
|
1069
1128
|
const participant = this.participants.get(id);
|
|
1070
1129
|
if (!runtime || !participant)
|
|
1071
1130
|
throw new Error("no such agent");
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1131
|
+
if (runtime.turnActive) {
|
|
1132
|
+
runtime.agent.cancel(runtime.sessionId);
|
|
1133
|
+
this.cancelPermissionsOf(id);
|
|
1134
|
+
this.notice(`${participant.name}: stop requested.`, "info");
|
|
1135
|
+
return "turn";
|
|
1136
|
+
}
|
|
1137
|
+
if (runtime.pendingTurn || runtime.delayTimer || this.floorQueue.includes(id)) {
|
|
1138
|
+
this.dropScheduledTurn(id);
|
|
1139
|
+
this.notice(`${participant.name}: stopped before it began; the turn is dropped.`, "info");
|
|
1140
|
+
this.postSystem(`${participant.name} was stopped by ${this.humanName} before it began.`, undefined, false, { tone: "attention" });
|
|
1141
|
+
return "queued";
|
|
1142
|
+
}
|
|
1143
|
+
this.notice(`${participant.name}: nothing to stop, it is not writing.`, "info");
|
|
1144
|
+
return "nothing";
|
|
1077
1145
|
}
|
|
1078
1146
|
async setConfig(id, configId, value) {
|
|
1079
1147
|
const runtime = this.runtimes.get(id);
|
|
@@ -1160,7 +1228,7 @@ export class Room extends EventEmitter {
|
|
|
1160
1228
|
if (wanted.length) {
|
|
1161
1229
|
if (this.hops >= this.hopLimit) {
|
|
1162
1230
|
const who = agentTargets.length ? message.toNames.join(", ") : "the other vibemates";
|
|
1163
|
-
this.postSystem(`Hop limit ${this.hopLimit} reached: ${who} will not be prompted until ${this.humanName} writes again
|
|
1231
|
+
this.postSystem(`Hop limit ${this.hopLimit} reached: ${who} will not be prompted until ${this.humanName} writes again.`, undefined, false, { tone: "attention" });
|
|
1164
1232
|
}
|
|
1165
1233
|
else {
|
|
1166
1234
|
this.hops += 1;
|
|
@@ -1245,6 +1313,25 @@ export class Room extends EventEmitter {
|
|
|
1245
1313
|
const result = lintRoomDesign(design, { ...this.designContext(kind), skills: this.skillsForDesign(participantId) });
|
|
1246
1314
|
return { ok: !result.errors.length, errors: result.errors.map((e) => e.message), warnings: result.warnings.map((w) => w.message), preview: result.preview };
|
|
1247
1315
|
}
|
|
1316
|
+
readMessageForAgent(participantId, seq, around) {
|
|
1317
|
+
const participant = this.agentInRoom(participantId);
|
|
1318
|
+
const window = agentReadableWindow(this.messages, seq, around);
|
|
1319
|
+
if (!window)
|
|
1320
|
+
throw new Error(`no message #${seq} in this room (or it is one the vibemates do not see)`);
|
|
1321
|
+
const view = (m) => ({
|
|
1322
|
+
seq: m.seq,
|
|
1323
|
+
from: m.fromName,
|
|
1324
|
+
to: m.toNames,
|
|
1325
|
+
at: new Date(m.ts).toISOString(),
|
|
1326
|
+
text: m.text,
|
|
1327
|
+
...(m.kind === "system" ? { kind: "system" } : {}),
|
|
1328
|
+
...(m.edited ? { edited: true } : {}),
|
|
1329
|
+
...(m.images && m.images.length ? { images: m.images.map((a, i) => ({ ref: `#${m.seq}.${a.n ?? i + 1}`, name: a.name, path: this.imagePath(a) })) } : {}),
|
|
1330
|
+
...(m.quotes && m.quotes.length ? { quotes: m.quotes.map((q) => ({ n: q.n, seq: q.seq, from: q.fromName, text: q.text })) } : {}),
|
|
1331
|
+
});
|
|
1332
|
+
this.log.info(`read_message: ${participant.name} read #${seq}${around ? ` (around ${around})` : ""}`);
|
|
1333
|
+
return { message: view(window.message), before: window.before.map(view), after: window.after.map(view) };
|
|
1334
|
+
}
|
|
1248
1335
|
designContext(kind) {
|
|
1249
1336
|
return {
|
|
1250
1337
|
kind,
|
|
@@ -1347,7 +1434,7 @@ export class Room extends EventEmitter {
|
|
|
1347
1434
|
this.proposals.set(proposal.key, proposal);
|
|
1348
1435
|
this.push({ type: "proposal", proposal });
|
|
1349
1436
|
const what = [...settingChanges.map((c) => c.key), ...vibes.ops.map((o) => `${o.op} ${o.name}`)].join(", ");
|
|
1350
|
-
this.postSystem(`${participant.name} proposes changes to the room (${what}); apply or reject them on the card.`, "human");
|
|
1437
|
+
this.postSystem(`${participant.name} proposes changes to the room (${what}); apply or reject them on the card.`, "human", false, { tone: "attention" });
|
|
1351
1438
|
this.log.info(`proposal ${proposal.key} from ${participant.name}: ${what}`);
|
|
1352
1439
|
return {
|
|
1353
1440
|
ok: true,
|
|
@@ -1839,6 +1926,7 @@ export class Room extends EventEmitter {
|
|
|
1839
1926
|
const attached = seesImages && (m.to.length === 0 || m.to.includes(id));
|
|
1840
1927
|
return m.images.map((a, i) => ({ n: a.n ?? i + 1, ref: `#${m.seq}.${a.n ?? i + 1}`, name: a.name, path: this.imagePath(a), mimeType: a.mimeType, attached, forNames: m.to.length ? m.toNames : [] }));
|
|
1841
1928
|
};
|
|
1929
|
+
const backlogQuotes = (m) => m.quotes && m.quotes.length ? m.quotes.map((q) => ({ n: q.n, seq: q.seq, fromName: q.fromName, ts: q.ts, text: q.text })) : undefined;
|
|
1842
1930
|
const prompt = composePrompt({
|
|
1843
1931
|
brief: briefReason ? buildBrief(settings, persona, roster, runtime.notesForBrief ?? (participant.notes && (participant.notesSeq ?? -1) >= runtime.lastBriefSeq ? participant.notes : undefined), skillsForPrompt) : undefined,
|
|
1844
1932
|
header: buildHeader(settings, persona, roster, this.hops, notes, skillsForPrompt),
|
|
@@ -1851,6 +1939,7 @@ export class Room extends EventEmitter {
|
|
|
1851
1939
|
toNames: m.toNames,
|
|
1852
1940
|
text: m.text,
|
|
1853
1941
|
images: backlogImages(m),
|
|
1942
|
+
quotes: backlogQuotes(m),
|
|
1854
1943
|
}),
|
|
1855
1944
|
omitted,
|
|
1856
1945
|
personaName: participant.name,
|
|
@@ -1950,7 +2039,7 @@ export class Room extends EventEmitter {
|
|
|
1950
2039
|
if (isContextFullError(failure))
|
|
1951
2040
|
this.contextFull(participant, runtime, failure ?? "");
|
|
1952
2041
|
else
|
|
1953
|
-
this.postSystem(`${participant.name} could not answer: ${(failure ?? "no result").slice(0, 240)}
|
|
2042
|
+
this.postSystem(`${participant.name} could not answer: ${(failure ?? "no result").slice(0, 240)}`, undefined, false, { tone: "error" });
|
|
1954
2043
|
return null;
|
|
1955
2044
|
}
|
|
1956
2045
|
return this.finalizeTurn(participant, runtime, draft, result, Date.now() - startedAt, retry, published, publishedAt);
|
|
@@ -2012,10 +2101,13 @@ export class Room extends EventEmitter {
|
|
|
2012
2101
|
if (retry) {
|
|
2013
2102
|
this.closeRetry(retry, cancelled ? "the correction turn was stopped; nothing was posted" : "the agent withdrew the reply");
|
|
2014
2103
|
if (cancelled)
|
|
2015
|
-
this.postSystem(`${participant.name} was stopped
|
|
2104
|
+
this.postSystem(`${participant.name} was stopped by ${this.humanName}.`, undefined, false, { tone: "attention" });
|
|
2105
|
+
}
|
|
2106
|
+
else if (cancelled) {
|
|
2107
|
+
this.postSystem(`${participant.name} was stopped by ${this.humanName}.`, undefined, false, { tone: "attention" });
|
|
2016
2108
|
}
|
|
2017
2109
|
else {
|
|
2018
|
-
this.postSystem(
|
|
2110
|
+
this.postSystem(`${participant.name} read the room and has nothing to add.`);
|
|
2019
2111
|
}
|
|
2020
2112
|
return null;
|
|
2021
2113
|
}
|
|
@@ -2025,7 +2117,7 @@ export class Room extends EventEmitter {
|
|
|
2025
2117
|
participant.failedTurns = (participant.failedTurns ?? 0) + 1;
|
|
2026
2118
|
participant.statusDetail = `agent error: ${text.replace(/\s+/g, " ").slice(0, 120)}${text.length > 120 ? "…" : ""}`;
|
|
2027
2119
|
this.push({ type: "participant", participant });
|
|
2028
|
-
this.postSystem(`${participant.name}'s agent reported an error instead of a reply: ${text.slice(0, 200)}${text.length > 200 ? "…" : ""}
|
|
2120
|
+
this.postSystem(`${participant.name}'s agent reported an error instead of a reply: ${text.slice(0, 200)}${text.length > 200 ? "…" : ""}`, undefined, false, { tone: "error" });
|
|
2029
2121
|
runtime.log.warn(`adapter error text treated as failed turn: ${text.slice(0, 200)}`);
|
|
2030
2122
|
if (retry)
|
|
2031
2123
|
this.closeRetry(retry, "the agent reported an error instead of a corrected reply");
|
|
@@ -2076,6 +2168,7 @@ export class Room extends EventEmitter {
|
|
|
2076
2168
|
text,
|
|
2077
2169
|
streaming: false,
|
|
2078
2170
|
stopReason: result.stopReason,
|
|
2171
|
+
...(cancelled ? { stoppedBy: this.humanName } : {}),
|
|
2079
2172
|
usage: result.usage ?? null,
|
|
2080
2173
|
durationMs,
|
|
2081
2174
|
};
|
|
@@ -2083,17 +2176,17 @@ export class Room extends EventEmitter {
|
|
|
2083
2176
|
if (publishedAt !== null && this.messages.some((x) => x.kind === "chat" && x.id !== message.id && x.ts > publishedAt)) {
|
|
2084
2177
|
const at = new Date(draft.ts);
|
|
2085
2178
|
const hhmm = `${String(at.getHours()).padStart(2, "0")}:${String(at.getMinutes()).padStart(2, "0")}`;
|
|
2086
|
-
this.postSystem(`${participant.name} finished the reply started at ${hhmm} · ${
|
|
2179
|
+
this.postSystem(`${participant.name} finished the reply started at ${hhmm} · ${formatDuration(durationMs)}`, "human", false, { refId: message.id, agentId: participant.id });
|
|
2087
2180
|
}
|
|
2088
2181
|
if (retry) {
|
|
2089
2182
|
this.closeRetry(retry, corrections.length ? `corrected reply posted, but it still breaks: ${corrections.map((c) => c.replace(/^reminder:\s*/i, "")).join("; ")}` : "corrected reply posted");
|
|
2090
2183
|
}
|
|
2091
2184
|
if (cancelled) {
|
|
2092
|
-
this.postSystem(`${participant.name} was stopped mid-reply;
|
|
2185
|
+
this.postSystem(`${participant.name} was stopped mid-reply by ${this.humanName}; what it had written stays in the room.`, "agents", false, { tone: "attention" });
|
|
2093
2186
|
return null;
|
|
2094
2187
|
}
|
|
2095
2188
|
if (result.stopReason !== "end_turn") {
|
|
2096
|
-
this.postSystem(`${participant.name} stopped with ${result.stopReason}
|
|
2189
|
+
this.postSystem(`${participant.name} stopped with ${result.stopReason}.`, undefined, false, { tone: "attention" });
|
|
2097
2190
|
}
|
|
2098
2191
|
this.route(message);
|
|
2099
2192
|
return null;
|
|
@@ -2271,12 +2364,12 @@ export class Room extends EventEmitter {
|
|
|
2271
2364
|
runtime.log.info(`usage dropped ${runtime.lastUsed} -> ${u.used}; brief scheduled`);
|
|
2272
2365
|
participant.contextEvent = { kind: "compacted", at: Date.now(), used: u.used, size: u.size };
|
|
2273
2366
|
runtime.notesDue = overThreshold(u.used, u.size);
|
|
2274
|
-
this.postSystem(`${participant.name} compacted its context (${Math.round(runtime.lastUsed / 1000)}k → ${Math.round(u.used / 1000)}k tokens); the room rules are re-sent with its next turn${participant.notes ? ", with its notes" : ""}.`, "human");
|
|
2367
|
+
this.postSystem(`${participant.name} compacted its context (${Math.round(runtime.lastUsed / 1000)}k → ${Math.round(u.used / 1000)}k tokens); the room rules are re-sent with its next turn${participant.notes ? ", with its notes" : ""}.`, "human", false, { tone: "attention" });
|
|
2275
2368
|
}
|
|
2276
2369
|
if (crossedThreshold(runtime.lastUsed, u.used, u.size)) {
|
|
2277
2370
|
runtime.notesDue = true;
|
|
2278
2371
|
participant.contextEvent = { kind: "threshold", at: Date.now(), used: u.used, size: u.size };
|
|
2279
|
-
this.postSystem(`${participant.name} is at ${Math.round((100 * u.used) / u.size)}% of its context; it will leave notes with its next reply. You can respawn it with memory from its panel.`, "human");
|
|
2372
|
+
this.postSystem(`${participant.name} is at ${Math.round((100 * u.used) / u.size)}% of its context; it will leave notes with its next reply. You can respawn it with memory from its panel.`, "human", false, { tone: "attention" });
|
|
2280
2373
|
runtime.log.info(`context at ${u.used}/${u.size}: notes due`);
|
|
2281
2374
|
}
|
|
2282
2375
|
runtime.lastUsed = u.used;
|
|
@@ -2443,8 +2536,17 @@ export class Room extends EventEmitter {
|
|
|
2443
2536
|
participant.effort = pick("thought_level") ?? participant.effort;
|
|
2444
2537
|
participant.mode = pick("mode") ?? participant.mode;
|
|
2445
2538
|
}
|
|
2446
|
-
failStart(participant, error, fresh) {
|
|
2539
|
+
failStart(participant, error, fresh, stderr = []) {
|
|
2447
2540
|
participant.statusDetail = error instanceof Error ? error.message : String(error);
|
|
2541
|
+
const recipe = getRecipe(participant.agentType ?? "");
|
|
2542
|
+
participant.trouble = classifyStartFailure({
|
|
2543
|
+
error: participant.statusDetail,
|
|
2544
|
+
stderr,
|
|
2545
|
+
vendor: recipe?.vendor ?? participant.agentVendor ?? "The coding agent",
|
|
2546
|
+
loginCommand: recipe?.loginCommand || undefined,
|
|
2547
|
+
installHint: recipe?.installHint || undefined,
|
|
2548
|
+
loginState: recipe?.loginState,
|
|
2549
|
+
});
|
|
2448
2550
|
this.notice(`${participant.name}: failed to start: ${participant.statusDetail}`, "error");
|
|
2449
2551
|
if (fresh) {
|
|
2450
2552
|
participant.status = "error";
|
|
@@ -2456,6 +2558,7 @@ export class Room extends EventEmitter {
|
|
|
2456
2558
|
participant.status = "offline";
|
|
2457
2559
|
this.push({ type: "participant", participant });
|
|
2458
2560
|
}
|
|
2561
|
+
return new Error(`${participant.trouble.what} ${participant.trouble.advice} (${participant.statusDetail})`);
|
|
2459
2562
|
}
|
|
2460
2563
|
cancelPermissionsOf(id) {
|
|
2461
2564
|
for (const [key, entry] of this.permissions) {
|
package/dist/rows.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
|
+
const LEGACY = [
|
|
3
|
+
[/^(Hush|Focus):/, "hush"],
|
|
4
|
+
[/ could not answer: /, "error"],
|
|
5
|
+
[/'s agent reported an error instead of a reply/, "error"],
|
|
6
|
+
[/ ran out of context/, "error"],
|
|
7
|
+
[/ was stopped\b/, "attention"],
|
|
8
|
+
[/^Hop limit \d+ reached/, "attention"],
|
|
9
|
+
[/ is at \d+% of its context/, "attention"],
|
|
10
|
+
[/ compacted its context/, "attention"],
|
|
11
|
+
[/ proposes changes to the room/, "attention"],
|
|
12
|
+
[/ was respawned while offline/, "attention"],
|
|
13
|
+
];
|
|
14
|
+
export function legacyRowTone(text) {
|
|
15
|
+
for (const [pattern, tone] of LEGACY)
|
|
16
|
+
if (pattern.test(text))
|
|
17
|
+
return tone;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|