This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -64,6 +64,7 @@ export interface PreCompactExtracted {
const DEFAULT_RECALL_LIMIT = 20;
const PER_HIT_CONTENT_BUDGET = 240;
const DEFAULT_SUMMARY_BUDGET_CHARS = 400;
const DEFAULT_HOOK_SIGNAL_TIMEOUT_MS = 500;
export interface SessionStartOpts {
recallLimit?: number;
@@ -213,7 +214,10 @@ export async function runStopBody(
memoryWorkspace: result.workspace,
cwd: payload.cwd,
},
{ senderId: `${a.source}-hook` },
{
senderId: `${a.source}-hook`,
timeoutMs: DEFAULT_HOOK_SIGNAL_TIMEOUT_MS,
},
);
if (emitted) ctx.logger.debug('stop signal emitted', { id: emitted.id });
}

View File

@@ -58,6 +58,24 @@ export interface HookRunOptions {
}
const DEFAULT_LOGGER_PREFIX = 'hive-mind-hooks';
const HOOK_CLI_TIMEOUT_MS = 2_500;
/**
* Lifecycle hooks must fail open before Codex/Cursor's 5-second host timeout.
* One bounded attempt prevents the bridge default (four attempts) from keeping
* the host waiting after a stalled CLI process.
*/
export function buildHookBridgeOptions(
logger: Logger,
cliPath?: string,
): CliBridgeOptions {
return {
logger,
timeout_ms: HOOK_CLI_TIMEOUT_MS,
max_retries: 0,
...(cliPath !== undefined ? { cli_path: cliPath } : {}),
};
}
/**
* Parse `--cli-path <value>` from argv. Used by hook scripts to thread
@@ -129,8 +147,7 @@ export async function runHook<TPayload, TStdoutPayload>(
const reader = opts.readStdin ?? readStdinAsString;
const argv = opts.argv ?? process.argv.slice(2);
const argvFlags = parseHookArgs(argv);
const bridgeOpts: CliBridgeOptions = { logger };
if (argvFlags.cliPath !== undefined) bridgeOpts.cli_path = argvFlags.cliPath;
const bridgeOpts = buildHookBridgeOptions(logger, argvFlags.cliPath);
const bridge = opts.bridge ?? createCliBridge(bridgeOpts);
try {

View File

@@ -27,3 +27,6 @@ export * from './handlers-core.js';
// Fail-open hook runner + stdin/argv helpers (re-authored from the CC _shared.ts).
export * from './hook-shared.js';
// Cross-platform entrypoint detection for executable hook modules.
export { isDirectExecution } from '@waggle/hive-mind-shim-core';

View File

@@ -370,4 +370,38 @@ describe('makeStopHandler — WAGGLE_SIGNAL_EMIT opt-in', () => {
);
expect(bridge.saveMemory).toHaveBeenCalledTimes(1);
});
it('returns below the host budget after a near-timeout save and a stalled signal', async () => {
const a = makeMockAdapter({ source: 'cursor' });
const bridge = makeMockBridge();
bridge.saveMemory.mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 2_400));
return { id: 'frame-slow', success: true, workspace: 'personal' };
});
const stalled = ((_url: string | URL | Request, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
const signal = init?.signal;
if (!signal) {
reject(new Error('missing abort signal'));
return;
}
const rejectAbort = (): void => reject(new DOMException('aborted', 'AbortError'));
if (signal.aborted) rejectAbort();
else signal.addEventListener('abort', rejectAbort, { once: true });
})) as typeof globalThis.fetch;
const h = makeStopHandler(a);
const startedAt = performance.now();
await withEnv('WAGGLE_SIGNAL_EMIT', '1', () =>
withCapturedFetch(stalled, () =>
h.run(h.parse({ response: 'never expose credentials.', cwd: '/p' }), makeCtx(bridge)),
),
);
const elapsedMs = performance.now() - startedAt;
expect(bridge.saveMemory).toHaveBeenCalledTimes(1);
expect(bridge.saveMemory.mock.calls[0][0].content).toContain('never expose credentials');
expect(elapsedMs).toBeGreaterThanOrEqual(2_300);
expect(elapsedMs).toBeLessThan(4_000);
}, 5_000);
});

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
buildHookBridgeOptions,
parseHookArgs,
pickStringField,
pickStringFromObject,
@@ -61,6 +62,27 @@ describe('parseHookArgs', () => {
});
});
describe('buildHookBridgeOptions', () => {
it('keeps lifecycle CLI work single-attempt and inside a 5s host budget', () => {
const logger = makeMockLogger();
expect(buildHookBridgeOptions(logger, 'C:\\Program Files\\Hive Mind\\cli.js')).toEqual({
logger,
cli_path: 'C:\\Program Files\\Hive Mind\\cli.js',
timeout_ms: 2_500,
max_retries: 0,
});
});
it('omits cli_path when the installer did not pin one', () => {
const logger = makeMockLogger();
expect(buildHookBridgeOptions(logger)).toEqual({
logger,
timeout_ms: 2_500,
max_retries: 0,
});
});
});
describe('pickStringField / pickStringFromObject', () => {
it('returns the first non-empty string match', () => {
expect(pickStringField({ a: 'x', b: 'y' }, 'a', 'b')).toBe('x');