volute 0.53.0 → 0.54.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/dist/auto-upgrade-HEVZKPTF.js +46 -0
- package/dist/{backup-WWXVIMYQ.js → backup-QRNX6RK7.js} +11 -11
- package/dist/channels-CKCY64TD.js +203 -0
- package/dist/{chat-WK2BLM7B.js → chat-ZUGNL5GS.js} +9 -9
- package/dist/{chunk-UTKUO2KY.js → chunk-3LHUXY3E.js} +2 -2
- package/dist/{chunk-PIZDPY4G.js → chunk-444RR3YF.js} +4 -4
- package/dist/{chunk-VDZ4I4ZP.js → chunk-56IT2ZXF.js} +3 -3
- package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
- package/dist/{chunk-LN7XJ56R.js → chunk-7XKPG66D.js} +9 -9
- package/dist/{chunk-R772ET2M.js → chunk-AAPNU7GA.js} +4 -0
- package/dist/{chunk-7ARONCZW.js → chunk-ATZF2RNJ.js} +3 -3
- package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
- package/dist/{chunk-RMADJFA7.js → chunk-CLCHOXLR.js} +1 -1
- package/dist/{chunk-IQ7VNWH6.js → chunk-HKGYZVWN.js} +1 -1
- package/dist/{chunk-HASADR3E.js → chunk-LGXVGIWN.js} +66 -25
- package/dist/{chunk-P2TBUKLW.js → chunk-LKUGUQUW.js} +3 -3
- package/dist/{chunk-AP2KQMMW.js → chunk-OAYY3WF3.js} +11 -3
- package/dist/{chunk-UO4MZQNO.js → chunk-OGPVJ53N.js} +14 -6
- package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
- package/dist/chunk-OT5SXF7L.js +904 -0
- package/dist/{chunk-EDR47UE2.js → chunk-OYF25B62.js} +1 -1
- package/dist/{chunk-TAWRJ24T.js → chunk-PPSN2MLC.js} +2 -2
- package/dist/{chunk-5DBGQBQR.js → chunk-RGHF3OK5.js} +21 -10
- package/dist/{chunk-RQRO6PC7.js → chunk-VDUCLVZI.js} +305 -31
- package/dist/{chunk-23UJGVYP.js → chunk-WHL6PKYP.js} +10 -10
- package/dist/{chunk-HTB7KDRU.js → chunk-WZZCA23D.js} +5 -4
- package/dist/{chunk-UDZLPR34.js → chunk-YSRYCS3N.js} +13 -8
- package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
- package/dist/{chunk-BDHKCLWK.js → chunk-ZNHL5KQJ.js} +1 -1
- package/dist/cli.js +19 -19
- package/dist/{clock-SH4NQC2F.js → clock-X7V3C7NL.js} +3 -3
- package/dist/{cloud-sync-MJVJ33LD.js → cloud-sync-52YNJRMR.js} +11 -11
- package/dist/{daemon-restart-BIGU4OKS.js → daemon-restart-QWHN5VMU.js} +4 -4
- package/dist/daemon.js +330 -746
- package/dist/{db-IPXRJUI7.js → db-I4EH5SLF.js} +3 -1
- package/dist/{delivery-manager-FDHCZ7PU.js → delivery-manager-7R2YIQ2W.js} +11 -7
- package/dist/{delivery-notices-65TB3CBX.js → delivery-notices-RZQ6GONP.js} +3 -3
- package/dist/{delivery-router-RHANNTAC.js → delivery-router-QS4NLJR6.js} +3 -1
- package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
- package/dist/{echo-text-KU6G4Z7V.js → echo-text-73CYQRL7.js} +14 -14
- package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
- package/dist/{extensions-VACUKFA5.js → extensions-QT65JGV6.js} +21 -11
- package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
- package/dist/{message-delivery-TUXM3AAE.js → message-delivery-TRKT4T2P.js} +11 -11
- package/dist/migrate-name-placeholder-C7NCNLWF.js +37 -0
- package/dist/{mind-IX5AWUST.js → mind-CT6ANXK4.js} +12 -12
- package/dist/{mind-contacts-BKUOUJPV.js → mind-contacts-PEXCDRXA.js} +8 -8
- package/dist/{mind-list-KNU34SUD.js → mind-list-7KBLPLES.js} +2 -2
- package/dist/{mind-service-WHFBUKZE.js → mind-service-7EFIJ3ZA.js} +11 -11
- package/dist/{mind-sleep-D3DEYAWG.js → mind-sleep-24HV4RCA.js} +5 -5
- package/dist/{mind-status-BPTLQ5Z6.js → mind-status-4YW4PHMO.js} +6 -3
- package/dist/{package-BN5KMU54.js → package-F6S5NHY2.js} +4 -4
- package/dist/{prompts-HNKGCAEY.js → prompts-JECJIFPQ.js} +1 -1
- package/dist/{scheduler-5X5UIQOQ.js → scheduler-73Q4MQ7Y.js} +6 -6
- package/dist/{seed-LHZ33UB4.js → seed-DRKAP3XV.js} +1 -1
- package/dist/{seed-cmd-U4RVJ3UA.js → seed-cmd-6IMTO3LG.js} +2 -2
- package/dist/{seed-create-OQU4KNCB.js → seed-create-VNLDYRFK.js} +1 -1
- package/dist/{seed-readiness-WJYYRRCK.js → seed-readiness-H5TTPAQH.js} +2 -2
- package/dist/{seed-sprout-SFUYH2RG.js → seed-sprout-PPGYGK4G.js} +8 -8
- package/dist/{send-FWZAMDBN.js → send-UXUQKO4N.js} +9 -9
- package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
- package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
- package/dist/{setup-RVSGCIF3.js → setup-YUAQMDVZ.js} +4 -4
- package/dist/skills/dreaming/SKILL.md +2 -2
- package/dist/skills/dreaming/references/INSTALL.md +1 -1
- package/dist/skills/tending/SKILL.md +2 -0
- package/dist/skills/volute-mind/references/integrations.md +3 -2
- package/dist/skills/volute-mind/references/routing.md +18 -5
- package/dist/{skills-QRPU6BWY.js → skills-GMQ5UIIW.js} +2 -2
- package/dist/{sleep-manager-NWUCRBZB.js → sleep-manager-KHI65FEX.js} +11 -11
- package/dist/{spirit-I5ECLGRI.js → spirit-Q5VGFWUI.js} +5 -3
- package/dist/{spirit-availability-NIUUYKU2.js → spirit-availability-G5FMWKRH.js} +12 -12
- package/dist/{sprout-A2PMEF7Z.js → sprout-PMTRL7QN.js} +1 -1
- package/dist/{src-LCSOQY6Z.js → src-3FIW7DLH.js} +241 -87
- package/dist/src-H3UCOP6B.js +516 -0
- package/dist/{src-HGNE2XG4.js → src-LTTQVN2Z.js} +1 -1
- package/dist/{status-EYG4A3OK.js → status-LCKTTIWS.js} +5 -5
- package/dist/{system-events-YTOU66QI.js → system-events-Q7IVZ5T4.js} +1 -1
- package/dist/{systems-2DIJY4RF.js → systems-HL7SGSW7.js} +6 -6
- package/dist/{turn-tracker-NNHS6MAR.js → turn-tracker-HZDPMSCC.js} +2 -2
- package/dist/{up-UGBHH743.js → up-XZDSJVMU.js} +3 -3
- package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
- package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
- package/dist/{variant-cleanup-CZDLIY3F.js → variant-cleanup-QHGGY7ND.js} +6 -6
- package/dist/{version-notify-SJVAQLZI.js → version-notify-VNVFX6UP.js} +19 -4
- package/dist/web-assets/assets/{index-b7G7ZsVk.js → index-CwtxGwe_.js} +16 -16
- package/dist/web-assets/assets/index-DhM1yYRN.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +4 -4
- package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
- package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +67 -0
- package/packages/extensions/{plan → intentions}/dist/ui/index.html +3 -3
- package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
- package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
- package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
- package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
- package/packages/extensions/pages/skills/pages/SKILL.md +18 -13
- package/templates/_base/.init/.config/prompts.json +2 -2
- package/templates/{pi → _base}/.init/.config/routes.json +1 -0
- package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/context-breakdown.ts +11 -3
- package/templates/_base/src/lib/routing.ts +2 -2
- package/templates/_base/src/lib/startup.ts +4 -2
- package/templates/claude/.init/CLAUDE.md +4 -0
- package/templates/claude/src/agent.ts +28 -12
- package/templates/claude/src/lib/message-channel.ts +43 -15
- package/templates/claude/src/lib/recover.ts +69 -0
- package/templates/claude/src/lib/stream-consumer.ts +23 -5
- package/templates/claude/volute-template.json +1 -1
- package/templates/codex/.init/AGENTS.md +4 -0
- package/templates/codex/volute-template.json +1 -1
- package/templates/pi/.init/MINDS.md +4 -0
- package/templates/pi/src/lib/mechanics-doc.ts +30 -0
- package/templates/pi/src/server.ts +3 -1
- package/templates/pi/volute-template.json +1 -1
- package/dist/channels-LW5ZLMSQ.js +0 -90
- package/dist/skills/plan-coordinator/SKILL.md +0 -60
- package/dist/src-UDMJZ6MH.js +0 -426
- package/dist/web-assets/assets/index-BHSU9r-4.css +0 -1
- package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
- package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
- package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
- package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
- package/templates/claude/.init/.config/routes.json +0 -11
- package/templates/codex/.init/.config/routes.json +0 -11
- package/dist/{accept-I4GVGWA3.js → accept-RSDIVJL6.js} +3 -3
- package/dist/{bridge-KU2IKSH3.js → bridge-YK7NTRU5.js} +3 -3
- package/dist/{create-C3XZACTV.js → create-M727VUDF.js} +3 -3
- package/dist/{env-BJGM6FBA.js → env-7AZFBPWX.js} +7 -7
- package/dist/{extension-B2W52WZY.js → extension-MRYJWSMV.js} +3 -3
- package/dist/{files-CHPNEQXN.js → files-PCKBQHAY.js} +3 -3
- package/dist/{list-R2GDWOEE.js → list-UQGIW7OF.js} +3 -3
- package/dist/{login-EZ4RRDQX.js → login-4NUS72Z5.js} +3 -3
- package/dist/{login-JDOAK465.js → login-57MMYILL.js} +4 -4
- package/dist/{logout-MWSIUOQU.js → logout-BYV433X6.js} +3 -3
- package/dist/{logout-K6ZGAJY5.js → logout-YFZAJRPY.js} +3 -3
- package/dist/{mind-history-RVF6KJ7K.js → mind-history-RZMMCHMX.js} +9 -9
- package/dist/{mind-wake-DCDFFQKX.js → mind-wake-LDGPRA42.js} +3 -3
- package/dist/{read-J5RM674N.js → read-RJ5NAW5J.js} +3 -3
- package/dist/{register-53LBGSDC.js → register-WNCSOTAU.js} +3 -3
- package/dist/{reject-K2A4UK2Y.js → reject-4NFLXKRN.js} +3 -3
- package/dist/{restart-R2WFNF5K.js → restart-B4GNMVF2.js} +7 -7
- package/dist/{skill-DXLLSUHB.js → skill-QYTBUDNQ.js} +7 -7
- package/dist/{start-XDOUAWN3.js → start-BDHJVOVX.js} +6 -6
- package/dist/{stop-O62RTEMD.js → stop-45QYSGYS.js} +7 -7
|
@@ -53,7 +53,7 @@ Messages are routed to named threads based on rules in `.config/routes.json`. Ea
|
|
|
53
53
|
|
|
54
54
|
## New Channels
|
|
55
55
|
|
|
56
|
-
When a message arrives from a channel you don't have a routing rule for, it's held rather than delivered — and because you haven't seen it, it isn't recorded in your history or counted as a message you received. You'll get a **[New channel: ...]** note in your main thread with the sender and a preview; it repeats (1st held message, then every 10th) so a channel stays visible. To start hearing it, add a rule for that channel to `.config/routes.json` — the backlog is released (the 10 most recent per channel; older ones stay readable via `volute chat
|
|
56
|
+
When a message arrives from a channel you don't have a routing rule for, it's held rather than delivered — and because you haven't seen it, it isn't recorded in your history or counted as a message you received. You'll get a **[New channel: ...]** note in your main thread with the sender and a preview; it repeats (1st held message, then every 10th) so a channel stays visible. To start hearing it, add a rule for that channel to `.config/routes.json` — the backlog is released (the 10 most recent per channel; older ones stay readable via `volute chat channels peek "<channel>"`) and recorded as inbound when you actually receive them. To stop the notes and archive the backlog, run `volute chat channels decline "<channel>"`; `volute chat channels list` shows what's currently held. Quote the channel in these commands — an unquoted `#name` is a shell comment and gets stripped before the CLI sees it. (To skip gating entirely and route everything to your default thread, set `"gateUnmatched": false`.)
|
|
57
57
|
|
|
58
58
|
## Variants
|
|
59
59
|
|
|
@@ -451,9 +451,17 @@ export async function processPiSession(
|
|
|
451
451
|
|
|
452
452
|
// --- Preamble text readers ---
|
|
453
453
|
|
|
454
|
-
/**
|
|
454
|
+
/**
|
|
455
|
+
* Read the instruction file the runtime loads on its own (CLAUDE.md for the
|
|
456
|
+
* Claude Agent SDK, AGENTS.md for codex).
|
|
457
|
+
*
|
|
458
|
+
* MINDS.md is deliberately absent: pi does not auto-load it, so the pi template
|
|
459
|
+
* appends it to the system prompt instead (see pi's `withMechanicsDoc`). Listing
|
|
460
|
+
* it here would count those tokens twice — once under `systemPrompt` and again
|
|
461
|
+
* under `sdkInstructions`.
|
|
462
|
+
*/
|
|
455
463
|
export function readSdkInstructions(cwd: string): string {
|
|
456
|
-
for (const name of ["CLAUDE.md", "
|
|
464
|
+
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
457
465
|
try {
|
|
458
466
|
return readFileSync(resolve(cwd, name), "utf-8");
|
|
459
467
|
} catch (err: any) {
|
|
@@ -497,7 +505,7 @@ export function countSystemPromptTokens(systemPrompt: string): number {
|
|
|
497
505
|
return countTokens(systemPrompt);
|
|
498
506
|
}
|
|
499
507
|
|
|
500
|
-
/** Count tokens in the
|
|
508
|
+
/** Count tokens in the runtime-loaded instruction file (CLAUDE.md or AGENTS.md). */
|
|
501
509
|
export function countSdkInstructionTokens(cwd: string): number {
|
|
502
510
|
const content = readSdkInstructions(cwd);
|
|
503
511
|
return content ? countTokens(content) : 0;
|
|
@@ -156,7 +156,7 @@ export function resolveSessionConfig(
|
|
|
156
156
|
config: RoutingConfig,
|
|
157
157
|
sessionName: string,
|
|
158
158
|
): ResolvedSessionConfig {
|
|
159
|
-
const defaults: ResolvedSessionConfig = { interrupt:
|
|
159
|
+
const defaults: ResolvedSessionConfig = { interrupt: false, replyInstructions: "once" };
|
|
160
160
|
|
|
161
161
|
if (!config.threads) return defaults;
|
|
162
162
|
|
|
@@ -165,7 +165,7 @@ export function resolveSessionConfig(
|
|
|
165
165
|
const batch = sessionConfig.batch != null ? normalizeBatch(sessionConfig.batch) : undefined;
|
|
166
166
|
return {
|
|
167
167
|
batch,
|
|
168
|
-
interrupt: sessionConfig.interrupt ??
|
|
168
|
+
interrupt: sessionConfig.interrupt ?? false,
|
|
169
169
|
instructions: sessionConfig.instructions,
|
|
170
170
|
replyInstructions: sessionConfig.replyInstructions ?? "once",
|
|
171
171
|
};
|
|
@@ -269,8 +269,10 @@ export const DEFAULT_PROMPTS: MindPrompts = {
|
|
|
269
269
|
"Context limit approaching — this session will rotate shortly. Turns before ${cutoff} will be collapsed to their summaries; turns from ${cutoff} on are kept verbatim in the continued session, so there's no need to re-describe them.\n\nFor the turns that will collapse, make sure they read the way you'd want: `volute mind history --provisional` shows the provisional summaries, and `volute mind history --write --turn <id> --text \"...\"` replaces any with your own account. Also save anything important to your files (memory/journal/${date}.md, or a memory/ file). Provisional summaries are kept if you write nothing — nothing blocks on this.\n\nYour MEMORY.md is currently ${memory_size}. It is loaded into every request, so consolidate rather than append — distill detail into memory/ files and keep MEMORY.md lean (see the memory skill).",
|
|
270
270
|
compaction_instructions:
|
|
271
271
|
"Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
|
|
272
|
+
// Quoted: `${channel}` is `#garden` for a Volute channel, and an unquoted `#` starts a
|
|
273
|
+
// shell comment, so the target would be stripped before the CLI saw it.
|
|
272
274
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${channel} prompt template
|
|
273
|
-
reply_instructions: 'To reply to this message, use: volute chat send ${channel} "your message"',
|
|
275
|
+
reply_instructions: 'To reply to this message, use: volute chat send "${channel}" "your message"',
|
|
274
276
|
event_instructions:
|
|
275
277
|
"This is a system event from your environment — not a message from anyone, and nothing awaits a reply. If it calls for action, use your normal channels. Your closing thoughts on an event turn are kept as a private reflection in your history.",
|
|
276
278
|
channel_invite: `[Channel Invite]
|
|
@@ -283,7 +285,7 @@ Further messages will be saved to \${filePath}
|
|
|
283
285
|
|
|
284
286
|
To accept, add to .config/routes.json:
|
|
285
287
|
Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
|
|
286
|
-
\${batchRecommendation}To respond, use: volute chat send \${channel} "your message"
|
|
288
|
+
\${batchRecommendation}To respond, use: volute chat send "\${channel}" "your message"
|
|
287
289
|
To reject, delete \${filePath}`,
|
|
288
290
|
};
|
|
289
291
|
|
|
@@ -11,6 +11,10 @@ Messages arrive with a context prefix:
|
|
|
11
11
|
|
|
12
12
|
You can also reach out proactively — see the **volute-mind** skill.
|
|
13
13
|
|
|
14
|
+
## Framework Upgrades
|
|
15
|
+
|
|
16
|
+
When the host updates Volute, your framework code (`src/`, plus `VOLUTE.md`) upgrades automatically the next time you're eligible — usually your next restart. Identity and memory files in `home/` — `SOUL.md`, `MEMORY.md`, everything you author — are never touched by an upgrade. If you'd rather manage your own framework code by hand, set `"upgrades": "manual"` in `.config/volute.json`.
|
|
17
|
+
|
|
14
18
|
## Identity & Sessions
|
|
15
19
|
|
|
16
20
|
These files shape your starting identity. They're loaded into your system prompt, but they belong to you — edit them as you evolve:
|
|
@@ -23,6 +23,7 @@ import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
|
|
|
23
23
|
import { createReplyInstructionsHook } from "./lib/hooks/reply-instructions.js";
|
|
24
24
|
import { log } from "./lib/logger.js";
|
|
25
25
|
import { createMessageChannel } from "./lib/message-channel.js";
|
|
26
|
+
import { relockstepMessageIds } from "./lib/recover.js";
|
|
26
27
|
import { buildSeededNote, type SeedCause } from "./lib/seed-note.js";
|
|
27
28
|
import {
|
|
28
29
|
isSessionReapable,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
import { createSessionStore } from "./lib/session-store.js";
|
|
40
41
|
import type { EffortLevel, ThinkingConfig } from "./lib/startup.js";
|
|
41
42
|
import { loadPrompts, renderCompactionWarning, type SubagentConfig } from "./lib/startup.js";
|
|
42
|
-
import { consumeStream } from "./lib/stream-consumer.js";
|
|
43
|
+
import { consumeStream, type MessageIdEntry } from "./lib/stream-consumer.js";
|
|
43
44
|
import type {
|
|
44
45
|
HandlerMeta,
|
|
45
46
|
HandlerResolver,
|
|
@@ -54,8 +55,9 @@ type Session = {
|
|
|
54
55
|
name: string;
|
|
55
56
|
channel: ReturnType<typeof createMessageChannel>;
|
|
56
57
|
listeners: Set<Listener>;
|
|
57
|
-
messageIds:
|
|
58
|
+
messageIds: MessageIdEntry[];
|
|
58
59
|
currentMessageId?: string;
|
|
60
|
+
currentSeq?: number;
|
|
59
61
|
currentQuery?: ReturnType<typeof query>;
|
|
60
62
|
messageChannels: Map<string, { channel: string; sender?: string }>;
|
|
61
63
|
replyInstructionsFired: boolean;
|
|
@@ -432,13 +434,13 @@ export function createMind(options: {
|
|
|
432
434
|
const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
|
|
433
435
|
session.rotationPhase =
|
|
434
436
|
session.currentMessageId !== undefined ? "warned" : "rotateAfterTurn";
|
|
435
|
-
session.
|
|
436
|
-
session.channel.push({
|
|
437
|
+
const seq = session.channel.push({
|
|
437
438
|
type: "user",
|
|
438
439
|
session_id: "",
|
|
439
440
|
message: { role: "user", content: [{ type: "text", text: warning }] },
|
|
440
441
|
parent_tool_use_id: null,
|
|
441
442
|
});
|
|
443
|
+
session.messageIds.push({ id: undefined, seq });
|
|
442
444
|
}
|
|
443
445
|
|
|
444
446
|
// PreCompact backstop: first fire warns + pins + enters the rotation machine
|
|
@@ -457,10 +459,11 @@ export function createMind(options: {
|
|
|
457
459
|
if (!session.name.startsWith("new-")) sessionStore.save(session.name, id);
|
|
458
460
|
},
|
|
459
461
|
broadcast: (event: VoluteEvent) => broadcastToSession(session, event),
|
|
462
|
+
// Identity-based ack — stream-consumer.ts calls this once per message the
|
|
463
|
+
// just-finished turn covers (its own driving message, plus any folded in
|
|
464
|
+
// mid-run) so none of them strand in the channel's in-flight set (#764).
|
|
465
|
+
ack: (seq: number) => session.channel.ack(seq),
|
|
460
466
|
onTurnEnd: async () => {
|
|
461
|
-
// This turn's message is fully processed — drop it from the channel's
|
|
462
|
-
// in-flight set so a later compaction abort won't re-feed it.
|
|
463
|
-
session.channel.ack();
|
|
464
467
|
session.lastActivityAt = Date.now();
|
|
465
468
|
// A turn resolved — the seeded note's injection had its chance to land
|
|
466
469
|
// (the pre-prompt hook ran and wasn't cancelled by an interrupt), so stop
|
|
@@ -602,8 +605,16 @@ export function createMind(options: {
|
|
|
602
605
|
// (the wrap-up warning plus anything that arrived mid-turn) so nothing is
|
|
603
606
|
// dropped when the killed subprocess takes its buffer with it.
|
|
604
607
|
const pending = session.channel.recover();
|
|
608
|
+
const oldMessageIds = session.messageIds;
|
|
605
609
|
session.channel = createMessageChannel();
|
|
606
|
-
|
|
610
|
+
// Starts from [] — nothing can race into this fresh channel/messageIds
|
|
611
|
+
// before the next line runs (single-threaded, no await in between).
|
|
612
|
+
session.messageIds = relockstepMessageIds(
|
|
613
|
+
pending,
|
|
614
|
+
oldMessageIds,
|
|
615
|
+
session.channel.push,
|
|
616
|
+
[],
|
|
617
|
+
);
|
|
607
618
|
continue; // restart the stream loop on the rotated session
|
|
608
619
|
}
|
|
609
620
|
throw err; // rethrow non-compaction errors
|
|
@@ -749,11 +760,16 @@ export function createMind(options: {
|
|
|
749
760
|
log("mind", `session "${session.name}": error reaping SDK subprocess:`, err),
|
|
750
761
|
);
|
|
751
762
|
// Nothing should have raced in (isSessionReapable checked isEmpty), but if it
|
|
752
|
-
// did, re-dispatch into a fresh session so no input is dropped.
|
|
763
|
+
// did, re-dispatch into a fresh session so no input is dropped. Same marker
|
|
764
|
+
// treatment as the rotation path (#764) — see relockstepMessageIds above.
|
|
765
|
+
// getOrCreateSession() can return a session an inbound message already raced
|
|
766
|
+
// into during the reapSessionQuery() await above (it pushed its own entry into
|
|
767
|
+
// fresh.messageIds via the normal handler path) — relockstepMessageIds appends
|
|
768
|
+
// onto fresh.messageIds rather than replacing it, so that entry survives.
|
|
753
769
|
const pending = session.channel.recover();
|
|
754
770
|
if (pending.length > 0) {
|
|
755
771
|
const fresh = getOrCreateSession(session.name);
|
|
756
|
-
|
|
772
|
+
relockstepMessageIds(pending, session.messageIds, fresh.channel.push, fresh.messageIds);
|
|
757
773
|
}
|
|
758
774
|
}
|
|
759
775
|
|
|
@@ -841,13 +857,13 @@ export function createMind(options: {
|
|
|
841
857
|
|
|
842
858
|
// Push message into SDK
|
|
843
859
|
session.lastActivityAt = Date.now();
|
|
844
|
-
session.
|
|
845
|
-
session.channel.push({
|
|
860
|
+
const seq = session.channel.push({
|
|
846
861
|
type: "user",
|
|
847
862
|
session_id: "",
|
|
848
863
|
message: { role: "user", content: toSDKContent(content) },
|
|
849
864
|
parent_tool_use_id: null,
|
|
850
865
|
});
|
|
866
|
+
session.messageIds.push({ id: meta.messageId, seq });
|
|
851
867
|
|
|
852
868
|
return () => {
|
|
853
869
|
if (filteredListener) session.listeners.delete(filteredListener);
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
|
|
3
|
+
/** A queued/in-flight message paired with the sequence number that identifies it. */
|
|
4
|
+
export type ChannelEntry = { msg: SDKUserMessage; seq: number };
|
|
5
|
+
|
|
6
|
+
// Module-scoped, not per-channel: rotation and the idle reaper both replace a
|
|
7
|
+
// session's channel with a fresh instance while carrying seqs forward (relocking
|
|
8
|
+
// old entries to new ones — see relockstepMessageIds in recover.ts). A per-instance
|
|
9
|
+
// counter would restart at 0 on every fresh channel, so a seq minted by one channel
|
|
10
|
+
// generation could collide with one minted by another; a single global counter makes
|
|
11
|
+
// every seq unique for the process's lifetime, so a stale seq can never be mistaken
|
|
12
|
+
// for a different message.
|
|
13
|
+
let nextSeq = 0;
|
|
14
|
+
|
|
3
15
|
export type MessageChannel = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
16
|
+
/** Returns the sequence number minted for this message — the identity `ack()` needs. */
|
|
17
|
+
push: (msg: SDKUserMessage) => number;
|
|
18
|
+
/**
|
|
19
|
+
* Acknowledge that the message with this `seq` has completed its turn. Identity-based
|
|
20
|
+
* (not positional): the SDK folds messages that arrive mid-run into the active run, so
|
|
21
|
+
* a turn's `result` can cover more than just the oldest in-flight message — see the
|
|
22
|
+
* caller in stream-consumer.ts.
|
|
23
|
+
*/
|
|
24
|
+
ack: (seq: number) => void;
|
|
7
25
|
/**
|
|
8
26
|
* Return every message that has not been fully processed — those delivered to
|
|
9
27
|
* the consumer but not yet acked (their turn never finished) followed by those
|
|
10
|
-
* still queued — and reset the channel. Used to re-feed
|
|
11
|
-
* compaction abort so in-flight input is never lost.
|
|
28
|
+
* still queued — paired with their `seq`, and reset the channel. Used to re-feed
|
|
29
|
+
* the stream after a compaction abort so in-flight input is never lost.
|
|
12
30
|
*/
|
|
13
|
-
recover: () =>
|
|
31
|
+
recover: () => ChannelEntry[];
|
|
14
32
|
/**
|
|
15
33
|
* Terminate the input iterable: resolve any pending `next()` with `done: true`
|
|
16
34
|
* and make subsequent `next()` calls return done. The SDK sees end-of-input and
|
|
@@ -23,33 +41,43 @@ export type MessageChannel = {
|
|
|
23
41
|
};
|
|
24
42
|
|
|
25
43
|
export function createMessageChannel(): MessageChannel {
|
|
26
|
-
const queue:
|
|
44
|
+
const queue: ChannelEntry[] = [];
|
|
27
45
|
// Messages handed to the consumer (the SDK's read-ahead) whose turn has not yet
|
|
28
46
|
// completed. The SDK's streamInput loop pulls messages eagerly and writes them
|
|
29
47
|
// to the CLI subprocess, so once delivered they no longer live in `queue`. We
|
|
30
48
|
// retain them here so a compaction abort — which kills that subprocess — can
|
|
31
49
|
// re-feed the unprocessed ones instead of dropping them.
|
|
32
|
-
const inFlight:
|
|
50
|
+
const inFlight: ChannelEntry[] = [];
|
|
33
51
|
let resolve: ((value: IteratorResult<SDKUserMessage>) => void) | null = null;
|
|
34
52
|
let closed = false;
|
|
35
53
|
|
|
36
|
-
function deliver(
|
|
37
|
-
inFlight.push(
|
|
38
|
-
return { value: msg, done: false };
|
|
54
|
+
function deliver(entry: ChannelEntry): IteratorResult<SDKUserMessage> {
|
|
55
|
+
inFlight.push(entry);
|
|
56
|
+
return { value: entry.msg, done: false };
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
return {
|
|
42
60
|
push(msg: SDKUserMessage) {
|
|
61
|
+
const entry: ChannelEntry = { msg, seq: nextSeq++ };
|
|
43
62
|
if (resolve) {
|
|
44
63
|
const r = resolve;
|
|
45
64
|
resolve = null;
|
|
46
|
-
r(deliver(
|
|
65
|
+
r(deliver(entry));
|
|
47
66
|
} else {
|
|
48
|
-
queue.push(
|
|
67
|
+
queue.push(entry);
|
|
49
68
|
}
|
|
69
|
+
return entry.seq;
|
|
50
70
|
},
|
|
51
|
-
ack() {
|
|
52
|
-
inFlight.
|
|
71
|
+
ack(seq: number) {
|
|
72
|
+
let i = inFlight.findIndex((e) => e.seq === seq);
|
|
73
|
+
if (i !== -1) {
|
|
74
|
+
inFlight.splice(i, 1);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Defensive: shouldn't normally be in the queue when acked, but remove it
|
|
78
|
+
// there too rather than leave a stranded entry if it somehow is.
|
|
79
|
+
i = queue.findIndex((e) => e.seq === seq);
|
|
80
|
+
if (i !== -1) queue.splice(i, 1);
|
|
53
81
|
},
|
|
54
82
|
recover() {
|
|
55
83
|
// Resolve any pending iterator wait with done:true so it doesn't
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { ChannelEntry } from "./message-channel.js";
|
|
3
|
+
import type { MessageIdEntry } from "./stream-consumer.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `recover()` returns messages the channel can't prove were unprocessed — a genuinely
|
|
7
|
+
* unfinished turn looks identical to a stranded, already-completed one would have before
|
|
8
|
+
* #764's ack fix. Mark every re-pushed message so the mind can check its own record
|
|
9
|
+
* before redoing work: fail toward noise (a spurious note is cosmetic and self-correcting)
|
|
10
|
+
* rather than silence (a missing one is invisible and produces a false permanent record).
|
|
11
|
+
*/
|
|
12
|
+
export const RECOVERED_MESSAGE_NOTE =
|
|
13
|
+
"Note: this message is being redelivered after a session interruption. You may already " +
|
|
14
|
+
"have handled it — check your recent work (e.g. journal, files, messages you've already " +
|
|
15
|
+
"sent) before repeating anything.";
|
|
16
|
+
|
|
17
|
+
export function markRecovered(msg: SDKUserMessage): SDKUserMessage {
|
|
18
|
+
const content = msg.message.content;
|
|
19
|
+
const blocks = typeof content === "string" ? [{ type: "text" as const, text: content }] : content;
|
|
20
|
+
// A message can be recovered twice (consecutive rotations, up to
|
|
21
|
+
// MAX_CONSECUTIVE_ROTATIONS) — don't stack the note on each pass.
|
|
22
|
+
const first = blocks[0];
|
|
23
|
+
if (first && "type" in first && first.type === "text" && first.text === RECOVERED_MESSAGE_NOTE) {
|
|
24
|
+
return { ...msg, message: { ...msg.message, content: blocks } };
|
|
25
|
+
}
|
|
26
|
+
const marker = { type: "text" as const, text: RECOVERED_MESSAGE_NOTE };
|
|
27
|
+
return { ...msg, message: { ...msg.message, content: [marker, ...blocks] } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Re-push `recover()`'s output into a fresh channel and append the result onto
|
|
32
|
+
* `existingMessageIds` — mutated in place and returned, rather than replacing it.
|
|
33
|
+
* That's not incidental: `existingMessageIds` may already hold an entry for a
|
|
34
|
+
* message that raced in and was pushed into the SAME fresh channel/messageIds
|
|
35
|
+
* while `recover()` was still pending (the idle reaper awaits the SDK subprocess's
|
|
36
|
+
* shutdown before recovering — see agent.ts). Returning a fresh array for the
|
|
37
|
+
* caller to assign over would silently discard that entry (#764) — appending
|
|
38
|
+
* in place makes that mistake impossible at the call site.
|
|
39
|
+
*
|
|
40
|
+
* Each entry's `id` carries over — matched by its *old* `seq` — to the *new* seq
|
|
41
|
+
* the fresh channel mints on push, so the next turn's currentMessageId/channel
|
|
42
|
+
* routing isn't corrupted by stale or missing ids. A `pending` entry with no
|
|
43
|
+
* match in `oldMessageIds` gets `id: undefined` (see the `idBySeq.get` fallback
|
|
44
|
+
* below) rather than throwing — safe because `seq` is now globally unique
|
|
45
|
+
* (message-channel.ts), so this can only happen if `oldMessageIds` itself was
|
|
46
|
+
* incomplete, and losing routing info for one message beats losing the message.
|
|
47
|
+
*
|
|
48
|
+
* `existingMessageIds` has no default — deliberately. `= []` would let a call site
|
|
49
|
+
* omit it and get back a fresh array to assign over, which is exactly the #764
|
|
50
|
+
* clobber: silently discarding whatever the target array already held. Making it
|
|
51
|
+
* required turns that omission into a compile error at every call site, including
|
|
52
|
+
* ones written later.
|
|
53
|
+
*/
|
|
54
|
+
export function relockstepMessageIds(
|
|
55
|
+
pending: ChannelEntry[],
|
|
56
|
+
oldMessageIds: MessageIdEntry[],
|
|
57
|
+
push: (msg: SDKUserMessage) => number,
|
|
58
|
+
existingMessageIds: MessageIdEntry[],
|
|
59
|
+
transform: (msg: SDKUserMessage) => SDKUserMessage = markRecovered,
|
|
60
|
+
): MessageIdEntry[] {
|
|
61
|
+
const idBySeq = new Map(oldMessageIds.map((e) => [e.seq, e.id]));
|
|
62
|
+
existingMessageIds.push(
|
|
63
|
+
...pending.map((entry) => ({
|
|
64
|
+
id: idBySeq.get(entry.seq),
|
|
65
|
+
seq: push(transform(entry.msg)),
|
|
66
|
+
})),
|
|
67
|
+
);
|
|
68
|
+
return existingMessageIds;
|
|
69
|
+
}
|
|
@@ -4,10 +4,14 @@ import { log, warn } from "./logger.js";
|
|
|
4
4
|
import { filterEvent, loadTransparencyPreset } from "./transparency.js";
|
|
5
5
|
import type { VoluteEvent } from "./types.js";
|
|
6
6
|
|
|
7
|
+
/** A pending message's daemon-facing id (routing/channel key) paired with its channel `seq`. */
|
|
8
|
+
export type MessageIdEntry = { id: string | undefined; seq: number };
|
|
9
|
+
|
|
7
10
|
export type StreamSession = {
|
|
8
11
|
name: string;
|
|
9
|
-
messageIds:
|
|
12
|
+
messageIds: MessageIdEntry[];
|
|
10
13
|
currentMessageId?: string;
|
|
14
|
+
currentSeq?: number;
|
|
11
15
|
messageChannels: Map<string, { channel: string; sender?: string }>;
|
|
12
16
|
};
|
|
13
17
|
|
|
@@ -16,6 +20,11 @@ export type StreamCallbacks = {
|
|
|
16
20
|
broadcast: (event: VoluteEvent) => void;
|
|
17
21
|
onTurnEnd?: () => void;
|
|
18
22
|
onContextTokens?: (tokens: number) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Acknowledge the message channel entry with this `seq` — its turn is done (either
|
|
25
|
+
* it drove this turn directly, or it was folded into it; see the `result` handler).
|
|
26
|
+
*/
|
|
27
|
+
ack: (seq: number) => void;
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
// Loaded once at startup — mind restarts on config changes
|
|
@@ -48,7 +57,9 @@ export async function consumeStream(
|
|
|
48
57
|
let preTurnPending = 0;
|
|
49
58
|
for await (const msg of stream) {
|
|
50
59
|
if (session.currentMessageId === undefined) {
|
|
51
|
-
|
|
60
|
+
const entry = session.messageIds.shift();
|
|
61
|
+
session.currentMessageId = entry?.id;
|
|
62
|
+
session.currentSeq = entry?.seq;
|
|
52
63
|
preTurnPending = session.messageIds.length;
|
|
53
64
|
}
|
|
54
65
|
if ("session_id" in msg && msg.session_id) {
|
|
@@ -123,14 +134,20 @@ export async function consumeStream(
|
|
|
123
134
|
if (session.currentMessageId) {
|
|
124
135
|
session.messageChannels.delete(session.currentMessageId);
|
|
125
136
|
}
|
|
137
|
+
// Ack this turn's driving message, identified by seq (not position — see
|
|
138
|
+
// message-channel.ts). Without this the channel's inFlight set strands an
|
|
139
|
+
// entry per turn, which gets replayed verbatim on the next rotation (#764).
|
|
140
|
+
if (session.currentSeq !== undefined) callbacks.ack(session.currentSeq);
|
|
126
141
|
// Prune every id folded into this turn — the ones pushed after it started
|
|
127
142
|
// — not just currentMessageId. The SDK folds mid-run arrivals into the
|
|
128
143
|
// active run (they never see a result of their own), and a stranded entry
|
|
129
144
|
// would be shifted in as the NEXT turn's id, tagging that turn's rows with
|
|
130
145
|
// the wrong channel (#700). Ids queued before the turn started stay: each
|
|
131
|
-
// still gets a run (and result) of its own.
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
// still gets a run (and result) of its own. Ack each folded entry too — same
|
|
147
|
+
// reasoning as above, applied to every message the fold absorbed.
|
|
148
|
+
for (const entry of session.messageIds.splice(preTurnPending)) {
|
|
149
|
+
if (entry.id !== undefined) session.messageChannels.delete(entry.id);
|
|
150
|
+
callbacks.ack(entry.seq);
|
|
134
151
|
}
|
|
135
152
|
log("mind", `session "${session.name}": turn done`);
|
|
136
153
|
// Log any error messages from the result
|
|
@@ -154,6 +171,7 @@ export async function consumeStream(
|
|
|
154
171
|
callbacks.broadcast({ type: "done" });
|
|
155
172
|
emit(session, { type: "done" });
|
|
156
173
|
session.currentMessageId = undefined;
|
|
174
|
+
session.currentSeq = undefined;
|
|
157
175
|
callbacks.onTurnEnd?.();
|
|
158
176
|
}
|
|
159
177
|
}
|
|
@@ -13,6 +13,10 @@ Messages arrive with a context prefix:
|
|
|
13
13
|
|
|
14
14
|
You can also reach out proactively — see the **volute-mind** skill.
|
|
15
15
|
|
|
16
|
+
## Framework Upgrades
|
|
17
|
+
|
|
18
|
+
When the host updates Volute, your framework code (`src/`, plus `VOLUTE.md`) upgrades automatically the next time you're eligible — usually your next restart. Identity and memory files in `home/` — `SOUL.md`, `MEMORY.md`, everything you author — are never touched by an upgrade. If you'd rather manage your own framework code by hand, set `"upgrades": "manual"` in `.config/volute.json`.
|
|
19
|
+
|
|
16
20
|
## Memory System
|
|
17
21
|
|
|
18
22
|
Two-tier memory, both managed via file tools:
|
|
@@ -13,6 +13,10 @@ Messages arrive with a context prefix:
|
|
|
13
13
|
|
|
14
14
|
You can also reach out proactively — see the **volute-mind** skill.
|
|
15
15
|
|
|
16
|
+
## Framework Upgrades
|
|
17
|
+
|
|
18
|
+
When the host updates Volute, your framework code (`src/`, plus `VOLUTE.md`) upgrades automatically the next time you're eligible — usually your next restart. Identity and memory files in `home/` — `SOUL.md`, `MEMORY.md`, everything you author — are never touched by an upgrade. If you'd rather manage your own framework code by hand, set `"upgrades": "manual"` in `.config/volute.json`.
|
|
19
|
+
|
|
16
20
|
## Memory System
|
|
17
21
|
|
|
18
22
|
Two-tier memory, both managed via file tools:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** This template's mechanics doc, relative to the mind's home/. */
|
|
5
|
+
export const MECHANICS_DOC = "MINDS.md";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Append the mechanics doc to the system prompt.
|
|
9
|
+
*
|
|
10
|
+
* pi-coding-agent only auto-loads a project context file named AGENTS.md or
|
|
11
|
+
* CLAUDE.md (see @earendil-works/pi-coding-agent `core/resource-loader.js`), so
|
|
12
|
+
* MINDS.md never reaches the model on its own — pi minds would run with no
|
|
13
|
+
* mechanics doc at all. The claude and codex templates get theirs for free
|
|
14
|
+
* because their runtimes read CLAUDE.md / AGENTS.md natively; pi has to be
|
|
15
|
+
* handed its doc by hand.
|
|
16
|
+
*
|
|
17
|
+
* Kept in the system prompt (rather than injected as a context file) so it is
|
|
18
|
+
* counted once, under `systemPrompt`, by the context breakdown.
|
|
19
|
+
*/
|
|
20
|
+
export function withMechanicsDoc(systemPrompt: string, homeDir: string): string {
|
|
21
|
+
const path = resolve(homeDir, MECHANICS_DOC);
|
|
22
|
+
let doc: string;
|
|
23
|
+
try {
|
|
24
|
+
doc = readFileSync(path, "utf-8").trim();
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
if (err?.code !== "ENOENT") throw err;
|
|
27
|
+
return systemPrompt;
|
|
28
|
+
}
|
|
29
|
+
return doc ? `${systemPrompt}\n\n---\n\n${doc}` : systemPrompt;
|
|
30
|
+
}
|
|
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { createMind } from "./agent.js";
|
|
3
3
|
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
4
4
|
import { log, setLevel } from "./lib/logger.js";
|
|
5
|
+
import { withMechanicsDoc } from "./lib/mechanics-doc.js";
|
|
5
6
|
import { createRouter } from "./lib/router.js";
|
|
6
7
|
import {
|
|
7
8
|
loadConfig,
|
|
@@ -18,7 +19,8 @@ if (config.logLevel) setLevel(config.logLevel);
|
|
|
18
19
|
if (config.model) log("server", `using model: ${config.model}`);
|
|
19
20
|
if (config.thinkingLevel) log("server", `thinking level: ${config.thinkingLevel}`);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// pi does not auto-load MINDS.md, so the mechanics doc is appended by hand.
|
|
23
|
+
const systemPrompt = withMechanicsDoc(loadSystemPrompt(config), resolve("home"));
|
|
22
24
|
const pkg = loadPackageInfo();
|
|
23
25
|
|
|
24
26
|
const mindDir = resolve(".");
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
resolveMindName
|
|
4
|
-
} from "./chunk-BTY4WNFE.js";
|
|
5
|
-
import {
|
|
6
|
-
command,
|
|
7
|
-
subcommands
|
|
8
|
-
} from "./chunk-TXSA4Q3V.js";
|
|
9
|
-
import "./chunk-O7IGP7ZW.js";
|
|
10
|
-
import {
|
|
11
|
-
daemonFetch
|
|
12
|
-
} from "./chunk-M74I3HHF.js";
|
|
13
|
-
import "./chunk-FNJCBRD3.js";
|
|
14
|
-
import "./chunk-K3NQKI34.js";
|
|
15
|
-
|
|
16
|
-
// packages/cli/src/commands/chat/channels.ts
|
|
17
|
-
var channelsListCmd = command({
|
|
18
|
-
name: "volute chat channels",
|
|
19
|
-
description: "List unrouted (gated) channels holding messages",
|
|
20
|
-
args: [],
|
|
21
|
-
flags: {
|
|
22
|
-
mind: { type: "string", description: "Mind name" }
|
|
23
|
-
},
|
|
24
|
-
run: async ({ flags }) => {
|
|
25
|
-
const mind = resolveMindName(flags);
|
|
26
|
-
const res = await daemonFetch(`/api/minds/${encodeURIComponent(mind)}/delivery/pending`);
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
const data = await res.json().catch(() => ({}));
|
|
29
|
-
console.error(data.error ?? `Failed to list gated channels: ${res.status}`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
const pending = await res.json();
|
|
33
|
-
if (pending.length === 0) {
|
|
34
|
-
console.log("No unrouted channels holding messages.");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const chW = Math.max(7, ...pending.map((p) => (p.channel ?? "unknown").length));
|
|
38
|
-
console.log(`${"CHANNEL".padEnd(chW)} HELD SINCE`);
|
|
39
|
-
for (const p of pending) {
|
|
40
|
-
console.log(
|
|
41
|
-
`${(p.channel ?? "unknown").padEnd(chW)} ${String(p.count).padStart(4)} ${p.firstSeen}`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
console.log(`
|
|
45
|
-
Route one to hear it, or 'volute chat channels decline <channel>' to opt out.`);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
var channelsDeclineCmd = command({
|
|
49
|
-
name: "volute chat channels decline",
|
|
50
|
-
description: "Decline an unrouted channel: stop invites and archive its held backlog",
|
|
51
|
-
args: [{ name: "channel", required: true, description: "Channel to decline (e.g. #bardo)" }],
|
|
52
|
-
flags: {
|
|
53
|
-
mind: { type: "string", description: "Mind name" }
|
|
54
|
-
},
|
|
55
|
-
run: async ({ args, flags }) => {
|
|
56
|
-
const mind = resolveMindName(flags);
|
|
57
|
-
const channel = args.channel;
|
|
58
|
-
const res = await daemonFetch(`/api/minds/${encodeURIComponent(mind)}/gates/decline`, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: { "Content-Type": "application/json" },
|
|
61
|
-
body: JSON.stringify({ channel })
|
|
62
|
-
});
|
|
63
|
-
if (!res.ok) {
|
|
64
|
-
const data2 = await res.json().catch(() => ({}));
|
|
65
|
-
console.error(data2.error ?? `Failed to decline channel: ${res.status}`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
const data = await res.json();
|
|
69
|
-
console.log(`Declined ${channel}; archived ${data.archived} held message(s).`);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
var cmd = subcommands({
|
|
73
|
-
name: "volute chat channels",
|
|
74
|
-
description: "Manage unrouted (gated) channels",
|
|
75
|
-
commands: {
|
|
76
|
-
list: {
|
|
77
|
-
description: "List unrouted channels holding messages",
|
|
78
|
-
run: channelsListCmd.execute
|
|
79
|
-
},
|
|
80
|
-
decline: {
|
|
81
|
-
description: "Decline an unrouted channel",
|
|
82
|
-
run: channelsDeclineCmd.execute
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
footer: "Use --mind <name> or VOLUTE_MIND to identify the mind."
|
|
86
|
-
});
|
|
87
|
-
var run = cmd.execute;
|
|
88
|
-
export {
|
|
89
|
-
run
|
|
90
|
-
};
|