aibroker 0.58.1 → 0.60.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/adapters/iterm/core.d.ts +12 -1
- package/dist/adapters/iterm/core.d.ts.map +1 -1
- package/dist/adapters/iterm/core.js +32 -5
- package/dist/adapters/iterm/core.js.map +1 -1
- package/dist/adapters/iterm/sessions.d.ts +28 -0
- package/dist/adapters/iterm/sessions.d.ts.map +1 -1
- package/dist/adapters/iterm/sessions.js +101 -4
- package/dist/adapters/iterm/sessions.js.map +1 -1
- package/dist/adapters/pailot/gateway.d.ts.map +1 -1
- package/dist/adapters/pailot/gateway.js +46 -11
- package/dist/adapters/pailot/gateway.js.map +1 -1
- package/dist/core/hybrid.d.ts +10 -1
- package/dist/core/hybrid.d.ts.map +1 -1
- package/dist/core/hybrid.js +50 -15
- package/dist/core/hybrid.js.map +1 -1
- package/dist/core/persistence.d.ts +51 -2
- package/dist/core/persistence.d.ts.map +1 -1
- package/dist/core/persistence.js +45 -1
- package/dist/core/persistence.js.map +1 -1
- package/dist/core/session-match.d.ts +23 -0
- package/dist/core/session-match.d.ts.map +1 -1
- package/dist/core/session-match.js +25 -11
- package/dist/core/session-match.js.map +1 -1
- package/dist/core/state.d.ts +7 -1
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +13 -3
- package/dist/core/state.js.map +1 -1
- package/dist/daemon/cli.js +3 -0
- package/dist/daemon/cli.js.map +1 -1
- package/dist/daemon/commands.d.ts.map +1 -1
- package/dist/daemon/commands.js +8 -0
- package/dist/daemon/commands.js.map +1 -1
- package/dist/daemon/core-handlers.d.ts +28 -0
- package/dist/daemon/core-handlers.d.ts.map +1 -1
- package/dist/daemon/core-handlers.js +215 -59
- package/dist/daemon/core-handlers.js.map +1 -1
- package/dist/daemon/dispatch.d.ts +60 -1
- package/dist/daemon/dispatch.d.ts.map +1 -1
- package/dist/daemon/dispatch.js +389 -18
- package/dist/daemon/dispatch.js.map +1 -1
- package/dist/daemon/inbound.d.ts +4 -0
- package/dist/daemon/inbound.d.ts.map +1 -1
- package/dist/daemon/inbound.js +134 -29
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +42 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/manage.d.ts +37 -0
- package/dist/daemon/manage.d.ts.map +1 -1
- package/dist/daemon/manage.js +179 -13
- package/dist/daemon/manage.js.map +1 -1
- package/dist/daemon/session-content.d.ts.map +1 -1
- package/dist/daemon/session-content.js +4 -2
- package/dist/daemon/session-content.js.map +1 -1
- package/dist/daemon/todoist-pending.d.ts +45 -0
- package/dist/daemon/todoist-pending.d.ts.map +1 -0
- package/dist/daemon/todoist-pending.js +58 -0
- package/dist/daemon/todoist-pending.js.map +1 -0
- package/dist/daemon/todoist-reply.d.ts +10 -0
- package/dist/daemon/todoist-reply.d.ts.map +1 -1
- package/dist/daemon/todoist-reply.js +1 -0
- package/dist/daemon/todoist-reply.js.map +1 -1
- package/dist/daemon/todoist-webhook.d.ts +29 -0
- package/dist/daemon/todoist-webhook.d.ts.map +1 -1
- package/dist/daemon/todoist-webhook.js +334 -209
- package/dist/daemon/todoist-webhook.js.map +1 -1
- package/dist/ipc/client.d.ts.map +1 -1
- package/dist/ipc/client.js +24 -3
- package/dist/ipc/client.js.map +1 -1
- package/dist/ipc/validate.d.ts +2 -0
- package/dist/ipc/validate.d.ts.map +1 -1
- package/dist/ipc/validate.js +6 -1
- package/dist/ipc/validate.js.map +1 -1
- package/dist/mcp/index.js +19 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/transport/screen.d.ts.map +1 -1
- package/dist/transport/screen.js +30 -4
- package/dist/transport/screen.js.map +1 -1
- package/dist/transport/sync-facade.d.ts +15 -2
- package/dist/transport/sync-facade.d.ts.map +1 -1
- package/dist/transport/sync-facade.js +16 -3
- package/dist/transport/sync-facade.js.map +1 -1
- package/hooks/drain-mailbox.mjs +34 -8
- package/package.json +2 -2
package/dist/daemon/manage.js
CHANGED
|
@@ -26,7 +26,7 @@ import { homedir } from "node:os";
|
|
|
26
26
|
import { log } from "../core/log.js";
|
|
27
27
|
import { timeCall } from "../core/call-timing.js";
|
|
28
28
|
import { readSessionContent } from "./session-content.js";
|
|
29
|
-
import { typeIntoSession, pasteTextIntoSession, sendControlU, sendEnterKey, escapeInputMode } from "../transport/sync-facade.js";
|
|
29
|
+
import { typeIntoSession, pasteTextIntoSession, sendControlU, sendEnterKey, escapeInputMode, wasLastEnumerationReliable } from "../transport/sync-facade.js";
|
|
30
30
|
import { discoverLiveSessions } from "../core/session-discovery.js";
|
|
31
31
|
import { hasPailotClients } from "../adapters/pailot/gateway.js";
|
|
32
32
|
import { getAibpBridge } from "../core/state.js";
|
|
@@ -847,6 +847,40 @@ export function promptUnsentText(content) {
|
|
|
847
847
|
export function promptHasUnsentText(content) {
|
|
848
848
|
return promptUnsentText(content) !== null;
|
|
849
849
|
}
|
|
850
|
+
/**
|
|
851
|
+
* Floor for {@link paneLooksIdle}: never call a pane idle before it has sat
|
|
852
|
+
* this long unchanged, no matter how empty it looks — a pane can be static
|
|
853
|
+
* for a few seconds between real turns too.
|
|
854
|
+
*/
|
|
855
|
+
export const PANE_IDLE_FLOOR_MS = 5 * 60_000;
|
|
856
|
+
// Claude Code's own working indicator: "Word… (12s · ↓ ...)" or
|
|
857
|
+
// "Word… (1m 3s · ↓ ...)". A finished turn reads "Word for Xm Ys · done
|
|
858
|
+
// H:MM ..." with no "…" before a paren, so it never matches this.
|
|
859
|
+
const SPINNER_RE = /…\s*\(\d+(?:m\s*\d+)?s/;
|
|
860
|
+
// promptUnsentText() treats the terminal's own "queued messages" hint as no
|
|
861
|
+
// text (it isn't something an operator typed), but for idleness it means the
|
|
862
|
+
// opposite: messages are still waiting to run, so the pane is not idle.
|
|
863
|
+
const QUEUED_HINT_RE = /❯\s*press up to edit/i;
|
|
864
|
+
/**
|
|
865
|
+
* Escape hatch for the arm gate ONLY. sessionIsWorking() reads any transcript
|
|
866
|
+
* activity within WORKING_RECENT_MS as "working", which holds for a session
|
|
867
|
+
* whose turn has already ended but which is running a long background shell
|
|
868
|
+
* (e.g. `pai worker run`) that keeps streaming progress into the transcript
|
|
869
|
+
* for up to an hour. This looks at the pane instead: quiet long enough,
|
|
870
|
+
* nothing typed, nothing spinning.
|
|
871
|
+
*/
|
|
872
|
+
export function paneLooksIdle(paneText, quietMs, floorMs = PANE_IDLE_FLOOR_MS) {
|
|
873
|
+
if (quietMs < floorMs)
|
|
874
|
+
return false;
|
|
875
|
+
if (promptUnsentText(paneText) !== null)
|
|
876
|
+
return false;
|
|
877
|
+
const lastLines = paneText.split("\n").slice(-8).join("\n");
|
|
878
|
+
if (QUEUED_HINT_RE.test(lastLines))
|
|
879
|
+
return false;
|
|
880
|
+
if (SPINNER_RE.test(lastLines))
|
|
881
|
+
return false;
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
850
884
|
/**
|
|
851
885
|
* Expand the date tokens in a handover path, against the clock right now.
|
|
852
886
|
*
|
|
@@ -1703,6 +1737,85 @@ export function writeStandingRules(text, path = RULES_FILE) {
|
|
|
1703
1737
|
* by a sentence does not walk back over it.
|
|
1704
1738
|
*/
|
|
1705
1739
|
const GOAL_MAX_CHARS = 3800;
|
|
1740
|
+
/**
|
|
1741
|
+
* The most a LINE TYPED AT A SESSION may be, in characters — much stricter
|
|
1742
|
+
* than GOAL_MAX_CHARS above, and for a different reason.
|
|
1743
|
+
*
|
|
1744
|
+
* Proven from a live transcript on 2026-09-24: on Claude Code 2.1.280,
|
|
1745
|
+
* pasting text over roughly this length gets converted into a
|
|
1746
|
+
* `<pasted_content>` attachment instead of typed input, so a leading `/goal`
|
|
1747
|
+
* is never read as a slash command — the whole thing lands as one plain
|
|
1748
|
+
* message ("[Pasted text #N]"), the session answers "Noted.", and no goal is
|
|
1749
|
+
* set at all. A ~1,150-char line failed this way that night; a ~900-char
|
|
1750
|
+
* line of the same shape still typed correctly on Claude Code 2.1.267 the
|
|
1751
|
+
* night before. 700 leaves headroom under both observed points.
|
|
1752
|
+
*/
|
|
1753
|
+
const GOAL_LINE_MAX_CHARS = 700;
|
|
1754
|
+
/** `/goal` plus whichever of the given bits are non-empty, one line, no doubled spaces. */
|
|
1755
|
+
function assembleGoalLine(agentish, objective, rulesPointer, screenGrant) {
|
|
1756
|
+
const bits = [agentish, objective, rulesPointer, screenGrant].filter((s) => s.length > 0);
|
|
1757
|
+
return oneLine(`/goal ${bits.join(" ")}`);
|
|
1758
|
+
}
|
|
1759
|
+
/** Cut `s` to at most `max` characters, breaking at a word and marking the cut. */
|
|
1760
|
+
function truncateAtWord(s, max) {
|
|
1761
|
+
if (max <= 0)
|
|
1762
|
+
return "…";
|
|
1763
|
+
if (s.length <= max)
|
|
1764
|
+
return s;
|
|
1765
|
+
const cut = s.slice(0, max - 1);
|
|
1766
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
1767
|
+
return `${lastSpace > 0 ? cut.slice(0, lastSpace) : cut}…`;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* The fitted goal line, and the (possibly shortened) objective that ended up
|
|
1771
|
+
* in it — the latter is what a caller must use to verify the arming landed,
|
|
1772
|
+
* since the fragment it waits for has to be a real substring of what was
|
|
1773
|
+
* actually typed.
|
|
1774
|
+
*
|
|
1775
|
+
* Shrinks in the order a reader can spare things: the AG2 header first (it
|
|
1776
|
+
* says nothing the session doesn't already know after its first arming), then
|
|
1777
|
+
* the rules pointer down to its shortest form, then the screen grant down to
|
|
1778
|
+
* just the time, and only as a last resort the objective itself — which is
|
|
1779
|
+
* the one thing nobody asked to have shortened, so it is never dropped
|
|
1780
|
+
* entirely, only trimmed at a word boundary.
|
|
1781
|
+
*/
|
|
1782
|
+
function fitGoal(parts, max) {
|
|
1783
|
+
const { objective } = parts;
|
|
1784
|
+
const agentish = parts.agentish;
|
|
1785
|
+
let rulesPointer = parts.rulesPointer;
|
|
1786
|
+
let screenGrant = parts.screenGrant ?? "";
|
|
1787
|
+
let line = assembleGoalLine(agentish, objective, rulesPointer, screenGrant);
|
|
1788
|
+
if (line.length <= max)
|
|
1789
|
+
return { line, objective };
|
|
1790
|
+
line = assembleGoalLine("", objective, rulesPointer, screenGrant);
|
|
1791
|
+
if (line.length <= max)
|
|
1792
|
+
return { line, objective };
|
|
1793
|
+
if (rulesPointer) {
|
|
1794
|
+
rulesPointer = "Rules: read ~/.aibroker/manage-rules.txt first.";
|
|
1795
|
+
line = assembleGoalLine("", objective, rulesPointer, screenGrant);
|
|
1796
|
+
if (line.length <= max)
|
|
1797
|
+
return { line, objective };
|
|
1798
|
+
}
|
|
1799
|
+
if (screenGrant) {
|
|
1800
|
+
const time = screenGrant.match(/until\s+(\d{1,2}:\d{2})/i)?.[1];
|
|
1801
|
+
if (time)
|
|
1802
|
+
screenGrant = `Screen: yours until ${time}.`;
|
|
1803
|
+
line = assembleGoalLine("", objective, rulesPointer, screenGrant);
|
|
1804
|
+
if (line.length <= max)
|
|
1805
|
+
return { line, objective };
|
|
1806
|
+
}
|
|
1807
|
+
const overhead = assembleGoalLine("", "", rulesPointer, screenGrant).length;
|
|
1808
|
+
const fittedObjective = truncateAtWord(objective, max - overhead - 1);
|
|
1809
|
+
return { line: assembleGoalLine("", fittedObjective, rulesPointer, screenGrant), objective: fittedObjective };
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Pure wrapper around {@link fitGoal} for callers that only want the line —
|
|
1813
|
+
* pinned by test/manage-goal-line-fit.test.ts so the shrink order above
|
|
1814
|
+
* cannot drift without a failing test.
|
|
1815
|
+
*/
|
|
1816
|
+
export function fitGoalLine(parts, max) {
|
|
1817
|
+
return fitGoal(parts, max).line;
|
|
1818
|
+
}
|
|
1706
1819
|
/**
|
|
1707
1820
|
* `until 08:00` — an END, rather than a length.
|
|
1708
1821
|
*
|
|
@@ -1879,7 +1992,16 @@ function screenStatus(m, now = Date.now()) {
|
|
|
1879
1992
|
}
|
|
1880
1993
|
return `screen: granted until ${at(lease.until)}, but ${describeControls(tool, now).replace(/^screen: /, "")} — renewing`;
|
|
1881
1994
|
}
|
|
1882
|
-
/**
|
|
1995
|
+
/**
|
|
1996
|
+
* The text actually typed at the session, and the fragment to look for
|
|
1997
|
+
* afterwards to know it landed.
|
|
1998
|
+
*
|
|
1999
|
+
* Short goal, context by reference — and, since GOAL_LINE_MAX_CHARS, always
|
|
2000
|
+
* a pointer to the rules rather than the rules themselves: inlining a real
|
|
2001
|
+
* standing-rules paragraph cannot fit a 700-char paste-safe budget anyway, so
|
|
2002
|
+
* there is no case left where trying the longer composeGoal() inline form
|
|
2003
|
+
* first would help.
|
|
2004
|
+
*/
|
|
1883
2005
|
function goalText(m) {
|
|
1884
2006
|
const extra = m.pending.length ? ` OPERATOR, since you were last armed: ${m.pending.join(" ")}` : "";
|
|
1885
2007
|
// The screen rule has to ride along with EVERY arming. Delivered once, it
|
|
@@ -1909,12 +2031,13 @@ function goalText(m) {
|
|
|
1909
2031
|
// AG2 goes first, ahead of the objective and the standing rules, on every
|
|
1910
2032
|
// arming — not once at setup. A managed session works unattended for hours
|
|
1911
2033
|
// and a reminder given only at the start does not survive a compaction or a
|
|
1912
|
-
// `/clear`.
|
|
1913
|
-
// spliced in afterwards, so the ordering it produces (task, then rules,
|
|
1914
|
-
// then right-now context) still holds with AG2 simply riding in front of
|
|
1915
|
-
// the task. What a managed session sends the operator, and what it commits
|
|
2034
|
+
// `/clear`. What a managed session sends the operator, and what it commits
|
|
1916
2035
|
// to git, stays prose either way — AG2 is only how agents talk to agents.
|
|
1917
|
-
|
|
2036
|
+
const rulesPointer = rules
|
|
2037
|
+
? `FIRST, before anything else: read ${standingRulesSource()} and follow every rule in it for the whole of this work — they are not optional and they are not summarised here.`
|
|
2038
|
+
: "";
|
|
2039
|
+
const fit = fitGoal({ objective: `${m.objective}${extra}`, agentish: AG2_SPEC, rulesPointer, screenGrant: hands }, GOAL_LINE_MAX_CHARS);
|
|
2040
|
+
return { text: fit.line, fragment: fit.objective.slice(0, 40) };
|
|
1918
2041
|
}
|
|
1919
2042
|
/**
|
|
1920
2043
|
* Did it land? Look for the goal's own words in the transcript.
|
|
@@ -1941,8 +2064,7 @@ async function sleep(ms) {
|
|
|
1941
2064
|
return new Promise((r) => setTimeout(r, ms));
|
|
1942
2065
|
}
|
|
1943
2066
|
async function arm(m, reason) {
|
|
1944
|
-
const text = goalText(m);
|
|
1945
|
-
const fragment = m.objective.slice(0, 40);
|
|
2067
|
+
const { text, fragment } = goalText(m);
|
|
1946
2068
|
/**
|
|
1947
2069
|
* NEVER TYPE A GOAL INTO A BARE SHELL.
|
|
1948
2070
|
*
|
|
@@ -2167,6 +2289,12 @@ function beat() {
|
|
|
2167
2289
|
*/
|
|
2168
2290
|
const DIALOG_EVERY_TICKS = 3;
|
|
2169
2291
|
let tickCount = 0;
|
|
2292
|
+
/**
|
|
2293
|
+
* Whether the operator has already been told about the current iTerm
|
|
2294
|
+
* enumeration failure — set once per episode, cleared on recovery, so a
|
|
2295
|
+
* standing fault alerts once rather than every 20s tick.
|
|
2296
|
+
*/
|
|
2297
|
+
let enumerationAlarmArmed = false;
|
|
2170
2298
|
function answerBlockingDialogs() {
|
|
2171
2299
|
if (Object.keys(state).length === 0)
|
|
2172
2300
|
return;
|
|
@@ -2187,6 +2315,21 @@ async function tick() {
|
|
|
2187
2315
|
const now = Date.now();
|
|
2188
2316
|
let dirty = false;
|
|
2189
2317
|
beat();
|
|
2318
|
+
// A failed osascript enumeration returns [] identically to a genuinely
|
|
2319
|
+
// empty machine — reported "ok" with 0 sessions is exactly the silent
|
|
2320
|
+
// failure this exists to catch. Reuses the flag every `sessions`/`status`
|
|
2321
|
+
// caller already sets; no separate probe.
|
|
2322
|
+
if (!wasLastEnumerationReliable()) {
|
|
2323
|
+
if (!enumerationAlarmArmed) {
|
|
2324
|
+
enumerationAlarmArmed = true;
|
|
2325
|
+
alertOperator("iTerm session enumeration is failing (AppleScript errors) — the session list may be empty or stale until this clears.");
|
|
2326
|
+
log("[manage] iTerm enumeration unreliable — alerted the operator");
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
else if (enumerationAlarmArmed) {
|
|
2330
|
+
enumerationAlarmArmed = false;
|
|
2331
|
+
log("[manage] iTerm enumeration recovered");
|
|
2332
|
+
}
|
|
2190
2333
|
if (++tickCount % DIALOG_EVERY_TICKS === 0) {
|
|
2191
2334
|
try {
|
|
2192
2335
|
answerBlockingDialogs();
|
|
@@ -2763,9 +2906,9 @@ async function tick() {
|
|
|
2763
2906
|
// an arming can never see it land. Logged at most once every 5 minutes
|
|
2764
2907
|
// (the skip itself runs every tick regardless) so a session in a long
|
|
2765
2908
|
// turn does not fill its own history with the same line.
|
|
2766
|
-
if (working) {
|
|
2909
|
+
if (working && !paneLooksIdle(content, now - m.lastChangeAt)) {
|
|
2767
2910
|
if (!m.busyNotedAt || now - m.busyNotedAt >= 5 * 60_000) {
|
|
2768
|
-
note(m, `busy — transcript moved <${Math.round(WORKING_RECENT_MS / 1000)}s ago; not arming`);
|
|
2911
|
+
note(m, `busy — transcript moved <${Math.round(WORKING_RECENT_MS / 1000)}s ago and pane not idle; not arming`);
|
|
2769
2912
|
m.busyNotedAt = now;
|
|
2770
2913
|
dirty = true;
|
|
2771
2914
|
}
|
|
@@ -2845,6 +2988,23 @@ export function startManagerLoop() {
|
|
|
2845
2988
|
}, TICK_MS);
|
|
2846
2989
|
timer.unref?.();
|
|
2847
2990
|
}
|
|
2991
|
+
/**
|
|
2992
|
+
* The off path's one job: make the session stop being managed, under every key
|
|
2993
|
+
* it may be filed under.
|
|
2994
|
+
*
|
|
2995
|
+
* managers.json is keyed by sessionId, and a relaunched pane gets a new one —
|
|
2996
|
+
* so `delete state[resolveSession(name)]` removes nothing (or a different,
|
|
2997
|
+
* newer entry) while the stale same-name entry survives and keeps arming the
|
|
2998
|
+
* session the operator just stopped. Returns the stale keys it removed so the
|
|
2999
|
+
* caller can name them in the log.
|
|
3000
|
+
*/
|
|
3001
|
+
export function stopManaging(s, sessionId, name) {
|
|
3002
|
+
const stale = Object.keys(s).filter((k) => k !== sessionId && s[k].name === name);
|
|
3003
|
+
delete s[sessionId];
|
|
3004
|
+
for (const k of stale)
|
|
3005
|
+
delete s[k];
|
|
3006
|
+
return stale;
|
|
3007
|
+
}
|
|
2848
3008
|
/**
|
|
2849
3009
|
* The whole operator surface, in one call.
|
|
2850
3010
|
*
|
|
@@ -3075,10 +3235,16 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
3075
3235
|
};
|
|
3076
3236
|
}
|
|
3077
3237
|
if (word === "off" || word === "stop") {
|
|
3078
|
-
|
|
3238
|
+
// Under a dead pane id the entry is still there, just not under the id
|
|
3239
|
+
// resolveSession returned — "was not being managed" is only true when no
|
|
3240
|
+
// key carries this name at all.
|
|
3241
|
+
const somewhere = !!existing || Object.keys(state).some((k) => state[k].name === name);
|
|
3242
|
+
if (!somewhere)
|
|
3079
3243
|
return { ok: true, message: `${name} was not being managed`, managed: false };
|
|
3080
|
-
|
|
3244
|
+
const stale = stopManaging(state, sessionId, name);
|
|
3081
3245
|
saveState(state);
|
|
3246
|
+
if (stale.length)
|
|
3247
|
+
log(`[manage:${name}] removed stale key(s) ${stale.join(", ")} — the pane was relaunched`);
|
|
3082
3248
|
log(`[manage:${name}] stopped by the operator`);
|
|
3083
3249
|
return { ok: true, message: `stopped managing ${name}`, managed: false };
|
|
3084
3250
|
}
|