@detergent-software/conductor 3.5.0-dev.2 → 3.5.0-dev.3
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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/hooks/model.ts +12 -0
- package/plugin/hooks/register.tsx +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Releases are tagged `conductor-v<version>` and staged on npm by `atk-conductor-release` (`pipelines/release-conductor.yml`), which writes each section below. `scripts/check-changelog.mjs` fails when a tag has no section here.
|
|
4
4
|
|
|
5
|
+
## [3.5.0-dev.3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.5.0-dev.2&targetVersion=GTconductor-v3.5.0-dev.3) (2026-10-29)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **conductor:** load the console skill again after a compaction (ATK-843) ([793a837](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/793a8377407f7a18c5fdffa09b9a9fd41ccafd00))
|
|
10
|
+
* **conductor:** read the skill's state before the compaction (ATK-843) ([95a10d7](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/95a10d79f462f136fb173cb31f71ba2623ae4bd1))
|
|
11
|
+
|
|
5
12
|
## [3.5.0-dev.2](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.5.0-dev.1&targetVersion=GTconductor-v3.5.0-dev.2) (2026-10-29)
|
|
6
13
|
|
|
7
14
|
### Features
|
package/README.md
CHANGED
|
@@ -955,7 +955,7 @@ The mod is the plugin's hooks module, code that costs no tokens, which does the
|
|
|
955
955
|
- **the band** above the prompt while the inbox has items: one hotkey per item (a plan approves after a second press, `y`, since a digit typed into an empty prompt presses a band button; an npm OTP copies its `npm stage approve` line; the rest open the pane), and `o` for the pane;
|
|
956
956
|
- **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, answer on a `scope`, `blocked`, `human` or `operator` Handoff, or a `failed` one whose worker waits on its item, which fills `/answer #<id> ` in the prompt, copy a command, open a link, done; on a Windows terminal, open is a button that opens a web link in the default browser once, since a ctrl+click on a link in a pane opened it twice, ATK-494), the workers by project (each project's rule has [pause] and [focus]), and today's spend. The header counts the workers running and parked and the inbox in words. Each worker is one line in columns that line up across projects, cut to the pane's width so it never wraps: a glyph, the item, its phase, its PR, what it waits on in words (`PR #557 to merge (skills-registry)`, `npm 8.9.0-dev.1 on dev (cli)`, `login (ado)`) or else its activity, and the age. As the pane narrows, the age goes first, then the wait, then the PR and the phase (ATK-521). The glyphs are one cell wide: `●` in a turn, `◌` up between turns, `!` stuck on a prompt, `○` parked, `×` failed or gone, `·` starting. A row expands to attach and why, its tokens today, its whole PR ref and its raw wait keys;
|
|
957
957
|
- **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/answer <item | #id> <text>` (`conduct done #<id> --answer <text>`, the text as typed), `/attach <item | worker>`, `/notify [<profile> [--until <when>]]`, `/pause [<project>] [--until <when>]`, `/unpause [<project>]`, `/focus-project <project> [--until <when>]` (puts its items first, until midnight without `--until`; not `/focus`, which is Claude Code's own);
|
|
958
|
-
- **the skill, loaded in code** (ATK-385, ATK-840): a console whose conversation lacks `/conductor:conductor-console` (one started by hand, as `claude --plugin-dir … -n conductor`,
|
|
958
|
+
- **the skill, loaded in code** (ATK-385, ATK-840, ATK-843): a console whose conversation lacks `/conductor:conductor-console` (one started by hand, as `claude --plugin-dir … -n conductor`, after a `/clear` or a resume, or after a compaction that kept only a summary of the skill: the mod checks what the compaction kept for the skill's first line of prose, and writes `conductor: the compaction kept|dropped /conductor:conductor-console` to the debug log) shows `console without its skill: /conductor:conductor-console loads with the next prompt` in the band, and the mod loads the skill itself with the next prompt, not the model. The operator's prompt carries the skill's text (its `SKILL.md` without the frontmatter, read from the plugin folder) as context the model reads first, with a line to run its Start, then answer. An escalation runs `/conductor:conductor-console` with the escalation as its arguments, since the mod's own `prompt.submit` hook never sees a prompt the mod submits. Nothing runs at the clear itself. A slash command loads nothing (`/conductor:conductor-console` loads itself): it carries a line asking the model to load the skill, as before, and so does every prompt while the file can't be read, or an escalation whose run the engine refuses;
|
|
959
959
|
- **a warm prompt cache while it idles** (ATK-386). The console's cache lasts an hour after its last use, and a wake after that writes the whole context again. So 55 minutes after the console's last turn, the mod sends one ping (`$.model.fork`, the console's own last request plus a one-word ask, no tools, no turn in the transcript). The API serves the ping from the cache for a tenth of a write's price, and the hour starts over. It pings again every 55 minutes until a turn comes, and each turn starts the timer over. At an idle point it runs `/clear` instead when the context is 150k or more, or after 12 pings in a row (the 13th idle point, about 12 hours after the last turn), and the band says `fresh console (cleared while idle: …)` until the next prompt, when the mod loads the skill (above). A ping that read less than half the context from the cache (the cache was gone anyway, as under a 5-minute TTL) stops the pings until the next turn. Each ping and clear goes to `POST /console/cache`, which logs a `console.cache` event with the context size and what the ping cost. A fork isn't in the transcript, so that event is the only place its cost shows;
|
|
960
960
|
- **the stream**: it runs `node <the cli.js that conductor.json names> events --follow --console` (`--console` tells the daemon a console is open), redraws on each event, toasts each new inbox item, and acts on three events:
|
|
961
961
|
|
package/package.json
CHANGED
package/plugin/hooks/model.ts
CHANGED
|
@@ -57,6 +57,18 @@ export const CONSOLE_SKILL_FILE = 'skills/conductor-console/SKILL.md';
|
|
|
57
57
|
export const NO_SKILL_CONTEXT = `This session is the conductor console (CONDUCTOR_CONSOLE=1), but /${CONSOLE_SKILL} hasn't run in it, so its rules don't hold. Load the ${CONSOLE_SKILL} skill with the Skill tool and follow it, then answer this prompt.`;
|
|
58
58
|
export const NO_SKILL_WARNING = `console without its skill: /${CONSOLE_SKILL} loads with the next prompt`;
|
|
59
59
|
|
|
60
|
+
// ATK-843: whether a conversation's texts still hold the skill after a compaction. The mark is the
|
|
61
|
+
// body's first line of prose, cut short: a summary paraphrases it, while it may repeat the heading.
|
|
62
|
+
// A first line too short to tell apart (a fence, a rule) marks nothing, so the skill counts as gone.
|
|
63
|
+
export function keepsSkill(texts: readonly string[], body: string): boolean {
|
|
64
|
+
const prose = body
|
|
65
|
+
.split(/\r?\n/)
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.find((line) => line !== '' && !line.startsWith('#'));
|
|
68
|
+
const mark = prose?.slice(0, 120);
|
|
69
|
+
return mark !== undefined && mark.length >= 40 && texts.some((text) => text.includes(mark));
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
// What the model reads beside the first prompt of a console without its skill: the skill, as the
|
|
61
73
|
// mod loaded it (ATK-840). It comes after the prompt, so it says to run Start first.
|
|
62
74
|
export function loadedSkillContext(body: string): string {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
idleAction,
|
|
26
26
|
inboxText,
|
|
27
27
|
isWebLink,
|
|
28
|
+
keepsSkill,
|
|
28
29
|
loadedSkillContext,
|
|
29
30
|
MAYBE_POSTED,
|
|
30
31
|
NO_SKILL_CONTEXT,
|
|
@@ -729,6 +730,35 @@ export const register: Register = (on) => {
|
|
|
729
730
|
return next(e);
|
|
730
731
|
});
|
|
731
732
|
|
|
733
|
+
// ATK-843: a compaction ends no conversation, but it can drop the skill: the mod's copy is a block
|
|
734
|
+
// beside one prompt, which a summary only paraphrases. What the compaction kept says which, and the
|
|
735
|
+
// next prompt loads the skill again when it went, as after a /clear. A skill file that can't be
|
|
736
|
+
// read counts as gone, so that prompt asks the model to load it.
|
|
737
|
+
// Whether the skill was there is read before the compaction runs: an escalation meanwhile marks it
|
|
738
|
+
// loaded for a run that comes after, which the compaction can't hold yet.
|
|
739
|
+
on('session.compact', async ($, e, next) => {
|
|
740
|
+
const isWatched = isConsole && e.agentId === undefined && e.trigger !== 'precompute';
|
|
741
|
+
const hadSkill = isWatched && (await read($, hasSkill));
|
|
742
|
+
const compacted = await next(e);
|
|
743
|
+
if (!hadSkill || compacted.skip !== undefined) return compacted;
|
|
744
|
+
// The skill's text in a message, or in a tool's result when the Skill tool loaded it.
|
|
745
|
+
const texts = compacted.messages.flatMap((message) => [
|
|
746
|
+
message.text,
|
|
747
|
+
...message.toolUses.map((use) => use.text ?? ''),
|
|
748
|
+
...(message.toolResults ?? []).map((result) => result.text),
|
|
749
|
+
]);
|
|
750
|
+
let isKept = false;
|
|
751
|
+
let unread = '';
|
|
752
|
+
try {
|
|
753
|
+
isKept = keepsSkill(texts, await loadSkill($));
|
|
754
|
+
} catch (error) {
|
|
755
|
+
unread = ` (its file can't be read: ${error instanceof Error ? error.message : String(error)})`;
|
|
756
|
+
}
|
|
757
|
+
$.ui.log(`conductor: the compaction ${isKept ? 'kept' : 'dropped'} /${CONSOLE_SKILL}${unread}`, { to: 'debug' });
|
|
758
|
+
if (!isKept) await update($, hasSkill, () => false);
|
|
759
|
+
return compacted;
|
|
760
|
+
});
|
|
761
|
+
|
|
732
762
|
// ATK-386: each of the console's own turns starts its idle point over, and a turn in progress
|
|
733
763
|
// holds it off. A subagent's turn raises no turn.start and its turn.complete carries its agentId;
|
|
734
764
|
// a ping's, should the engine raise one, carries the ping's prompt.
|