Merge branch 'main' into setup-scratch-agent-cleanup
This commit is contained in:
@@ -226,8 +226,12 @@ function createPreCompactHook(assistantName?: string): HookCallback {
|
|||||||
/**
|
/**
|
||||||
* Claude Code auto-compacts context at this window (tokens). Kept here so
|
* Claude Code auto-compacts context at this window (tokens). Kept here so
|
||||||
* the generic bootstrap doesn't need to know about Claude-specific env vars.
|
* the generic bootstrap doesn't need to know about Claude-specific env vars.
|
||||||
|
*
|
||||||
|
* Operator override: set CLAUDE_CODE_AUTO_COMPACT_WINDOW in the host env to
|
||||||
|
* raise or lower the threshold without editing source — useful when running
|
||||||
|
* with a 1M-context model variant or when emergency-tuning a deployment.
|
||||||
*/
|
*/
|
||||||
const CLAUDE_CODE_AUTO_COMPACT_WINDOW = '165000';
|
const CLAUDE_CODE_AUTO_COMPACT_WINDOW = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW || '165000';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stale-session detection. Matches Claude Code's error text when a
|
* Stale-session detection. Matches Claude Code's error text when a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nanoclaw",
|
"name": "nanoclaw",
|
||||||
"version": "2.0.15",
|
"version": "2.0.17",
|
||||||
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.33.0",
|
"packageManager": "pnpm@10.33.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="133k tokens, 67% of context window">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="135k tokens, 67% of context window">
|
||||||
<title>133k tokens, 67% of context window</title>
|
<title>135k tokens, 67% of context window</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||||
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
|
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
|
||||||
<text x="26" y="14">tokens</text>
|
<text x="26" y="14">tokens</text>
|
||||||
<text aria-hidden="true" x="71" y="15" fill="#010101" fill-opacity=".3">133k</text>
|
<text aria-hidden="true" x="71" y="15" fill="#010101" fill-opacity=".3">135k</text>
|
||||||
<text x="71" y="14">133k</text>
|
<text x="71" y="14">135k</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -58,7 +58,7 @@ const activeContainers = new Map<string, { process: ChildProcess; containerName:
|
|||||||
* a duplicate container against the same session directory, producing
|
* a duplicate container against the same session directory, producing
|
||||||
* racy double-replies.
|
* racy double-replies.
|
||||||
*/
|
*/
|
||||||
const wakePromises = new Map<string, Promise<void>>();
|
const wakePromises = new Map<string, Promise<boolean>>();
|
||||||
|
|
||||||
export function getActiveContainerCount(): number {
|
export function getActiveContainerCount(): number {
|
||||||
return activeContainers.size;
|
return activeContainers.size;
|
||||||
@@ -73,20 +73,32 @@ export function isContainerRunning(sessionId: string): boolean {
|
|||||||
* (the in-flight wake promise is reused).
|
* (the in-flight wake promise is reused).
|
||||||
*
|
*
|
||||||
* The container runs the v2 agent-runner which polls the session DB.
|
* The container runs the v2 agent-runner which polls the session DB.
|
||||||
|
*
|
||||||
|
* Contract: never throws. Returns `true` on successful spawn, `false` on
|
||||||
|
* transient spawn failure (e.g. OneCLI gateway unreachable). Callers don't
|
||||||
|
* need to wrap — the inbound row stays pending and host-sweep retries on
|
||||||
|
* its next tick. Callers that care (e.g. the router's typing indicator)
|
||||||
|
* can branch on the boolean.
|
||||||
*/
|
*/
|
||||||
export function wakeContainer(session: Session): Promise<void> {
|
export function wakeContainer(session: Session): Promise<boolean> {
|
||||||
if (activeContainers.has(session.id)) {
|
if (activeContainers.has(session.id)) {
|
||||||
log.debug('Container already running', { sessionId: session.id });
|
log.debug('Container already running', { sessionId: session.id });
|
||||||
return Promise.resolve();
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
const existing = wakePromises.get(session.id);
|
const existing = wakePromises.get(session.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
log.debug('Container wake already in-flight — joining existing promise', { sessionId: session.id });
|
log.debug('Container wake already in-flight — joining existing promise', { sessionId: session.id });
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
const promise = spawnContainer(session).finally(() => {
|
const promise = spawnContainer(session)
|
||||||
wakePromises.delete(session.id);
|
.then(() => true)
|
||||||
});
|
.catch((err) => {
|
||||||
|
log.warn('wakeContainer failed — host-sweep will retry', { sessionId: session.id, err });
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
wakePromises.delete(session.id);
|
||||||
|
});
|
||||||
wakePromises.set(session.id, promise);
|
wakePromises.set(session.id, promise);
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
@@ -435,20 +447,18 @@ async function buildContainerArgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OneCLI gateway — injects HTTPS_PROXY + certs so container API calls
|
// OneCLI gateway — injects HTTPS_PROXY + certs so container API calls
|
||||||
// are routed through the agent vault for credential injection.
|
// are routed through the agent vault for credential injection. Treated as
|
||||||
try {
|
// a transient hard failure: if we can't wire the gateway, we don't spawn.
|
||||||
if (agentIdentifier) {
|
// The caller (router or host-sweep) catches the throw, leaves the inbound
|
||||||
await onecli.ensureAgent({ name: agentGroup.name, identifier: agentIdentifier });
|
// message pending, and the next sweep tick retries.
|
||||||
}
|
if (agentIdentifier) {
|
||||||
const onecliApplied = await onecli.applyContainerConfig(args, { addHostMapping: false, agent: agentIdentifier });
|
await onecli.ensureAgent({ name: agentGroup.name, identifier: agentIdentifier });
|
||||||
if (onecliApplied) {
|
|
||||||
log.info('OneCLI gateway applied', { containerName });
|
|
||||||
} else {
|
|
||||||
log.warn('OneCLI gateway not applied — container will have no credentials', { containerName });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
log.warn('OneCLI gateway error — container will have no credentials', { containerName, err });
|
|
||||||
}
|
}
|
||||||
|
const onecliApplied = await onecli.applyContainerConfig(args, { addHostMapping: false, agent: agentIdentifier });
|
||||||
|
if (!onecliApplied) {
|
||||||
|
throw new Error('OneCLI gateway not applied — refusing to spawn container without credentials');
|
||||||
|
}
|
||||||
|
log.info('OneCLI gateway applied', { containerName });
|
||||||
|
|
||||||
// Host gateway
|
// Host gateway
|
||||||
args.push(...hostGatewayArgs());
|
args.push(...hostGatewayArgs());
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ async function sweepSession(session: Session): Promise<void> {
|
|||||||
const dueCount = countDueMessages(inDb);
|
const dueCount = countDueMessages(inDb);
|
||||||
if (dueCount > 0 && !isContainerRunning(session.id)) {
|
if (dueCount > 0 && !isContainerRunning(session.id)) {
|
||||||
log.info('Waking container for due messages', { sessionId: session.id, count: dueCount });
|
log.info('Waking container for due messages', { sessionId: session.id, count: dueCount });
|
||||||
|
// wakeContainer never throws — transient spawn failures (OneCLI down,
|
||||||
|
// etc.) return false and leave messages pending for the next tick.
|
||||||
await wakeContainer(session);
|
await wakeContainer(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
getMessagingGroupWithAgentCount,
|
getMessagingGroupWithAgentCount,
|
||||||
} from './db/messaging-groups.js';
|
} from './db/messaging-groups.js';
|
||||||
import { findSessionForAgent } from './db/sessions.js';
|
import { findSessionForAgent } from './db/sessions.js';
|
||||||
import { startTypingRefresh } from './modules/typing/index.js';
|
import { startTypingRefresh, stopTypingRefresh } from './modules/typing/index.js';
|
||||||
import { log } from './log.js';
|
import { log } from './log.js';
|
||||||
import { resolveSession, writeSessionMessage, writeOutboundDirect } from './session-manager.js';
|
import { resolveSession, writeSessionMessage, writeOutboundDirect } from './session-manager.js';
|
||||||
import { wakeContainer } from './container-runner.js';
|
import { wakeContainer } from './container-runner.js';
|
||||||
@@ -457,7 +457,11 @@ async function deliverToAgent(
|
|||||||
startTypingRefresh(session.id, session.agent_group_id, event.channelType, event.platformId, event.threadId);
|
startTypingRefresh(session.id, session.agent_group_id, event.channelType, event.platformId, event.threadId);
|
||||||
const freshSession = getSession(session.id);
|
const freshSession = getSession(session.id);
|
||||||
if (freshSession) {
|
if (freshSession) {
|
||||||
await wakeContainer(freshSession);
|
const woke = await wakeContainer(freshSession);
|
||||||
|
// wakeContainer never throws — it returns false on transient spawn
|
||||||
|
// failure (host-sweep retries). Stop the typing indicator we just
|
||||||
|
// started so it doesn't leak; the inbound row stays pending.
|
||||||
|
if (!woke) stopTypingRefresh(freshSession.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user