рождение работника проходит через ворота счёта: порядок шагов бережёт кошелёк владельца
This commit is contained in:
+53
-17
@@ -8,7 +8,9 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { resolveBin, needsShell } from './paths.mjs';
|
||||
import { resolveBin, needsShell, configPath } from './paths.mjs';
|
||||
import { readJson } from './state.mjs';
|
||||
import { rozhdenieSoSchyotom, spawnAttemptsPath } from './schyot-zamerov.mjs';
|
||||
|
||||
export function resolveClaudeBin({ lookIn = null, name = 'claude' } = {}) {
|
||||
return resolveBin(name, lookIn ? { lookIn } : {});
|
||||
@@ -136,25 +138,59 @@ export function isRealSuccess(result) {
|
||||
// из настроек владельца и проносится сюда насквозь: run.mjs (задача 14) читает `worker_model`
|
||||
// из night.config.json → кладёт в карточку работника → supervisor.mjs (задача 12) берёт
|
||||
// из карточки → сюда. Ни одно звено цепочки своего слова не подставляет.
|
||||
export function startWorker({ cwd, prompt, settingsPath, sessionId, dbName, model, addDirs = [] }) {
|
||||
//
|
||||
// ━━━ 🔴🔴 ЗДЕСЬ И ЕСТЬ РОЖДЕНИЕ РАБОТНИКА — ЕДИНСТВЕННОЕ В ЗАТЕЕ ━━━
|
||||
// Замерено командой 02.08.2026: живых запусков головы в затее ровно ДВА — крошечный платный
|
||||
// замер остатка (`budget.mjs`) и вот этот. Замер был сосчитан и ограничен числом 30,
|
||||
// а рождение работника — расход в разы больший, на ЧАСЫ работы — не считалось ничем.
|
||||
// То есть предел владельца сторожил копейки и не сторожил рубли. Теперь и здесь стоят
|
||||
// ворота счёта (`rozhdenieSoSchyotom`), и они те же самые, что у замера: второго устройства
|
||||
// для этого нарочно не заведено.
|
||||
//
|
||||
// 🔴 ПОРЯДОК ШАГОВ — НЕ СЛУЧАЙНЫЙ. Сперва идут все отказы, которые НЕ СТОЯТ ДЕНЕГ (программу
|
||||
// не нашли, голову не сказали, имя базы не дали), и только потом счёт. Иначе описка
|
||||
// в настройках владельца воровала бы у него рождение, которого не было.
|
||||
export function startWorker({
|
||||
cwd, prompt, settingsPath, sessionId, dbName, model, addDirs = [],
|
||||
// 🔴 Настройки и счёт — доводы с НАСТОЯЩИМИ умолчаниями, то есть боевое поведение
|
||||
// не изменилось ни на строку: надзиратель зовёт `startWorker` как звал, без новых слов.
|
||||
// Подавать их своими нужно ровно одному — проверкам: писать в настоящее хозяйство
|
||||
// `.night` им нельзя, оно одно на весь рабочий угол. Второй дороги к рождению эти два
|
||||
// довода не открывают: счёт идёт ВСЕГДА, они лишь говорят, в какой тетради он ведётся.
|
||||
cfg = readJson(configPath()) ?? {},
|
||||
attemptsPath = spawnAttemptsPath(),
|
||||
}) {
|
||||
// Не голое имя и не голый путь: `claudeLaunch` отвечает ещё и на вопрос «а запустится ли
|
||||
// найденное». Без него `.cmd` даёт EINVAL, и работник не рождается вовсе.
|
||||
const launch = claudeLaunch();
|
||||
const args = [...launch.prefixArgs, ...buildArgs({ prompt, settingsPath, sessionId, model, addDirs })];
|
||||
// detached: false — работник обязан быть ребёнком НАДЗИРАТЕЛЯ. Отвяжи его,
|
||||
// и надзиратель перестанет видеть и его поток, и его смерть, а значит перестанет
|
||||
// исполнять пределы. Отвязанным живёт надзиратель, не работник.
|
||||
const child = spawn(launch.command, args, {
|
||||
cwd,
|
||||
detached: false,
|
||||
windowsHide: true,
|
||||
shell: launch.shell,
|
||||
// Окружение — урезанным списком, а не наследованием. Иначе работник получил бы все
|
||||
// боевые ключи из переменных среды запускающего, а файловый замок этого не увидел бы.
|
||||
env: workerEnv({ dbName }),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
// Окружение — урезанным списком, а не наследованием. Иначе работник получил бы все
|
||||
// боевые ключи из переменных среды запускающего, а файловый замок этого не увидел бы.
|
||||
// 🔴 Считается ЗАРАНЕЕ, до счёта: без имени своей базы `workerEnv` падает, и этот отказ
|
||||
// денег не стоит — записывать его в счёт значило бы украсть у владельца рождение.
|
||||
const env = workerEnv({ dbName });
|
||||
|
||||
// 🔴🔴 ВОРОТА СЧЁТА. Пропустят — родим, откажут — бросят человеческой строкой, которую
|
||||
// надзиратель уже умеет ловить (`worker_spawn_failed`) и кладёт словами в утреннюю сводку.
|
||||
return rozhdenieSoSchyotom({
|
||||
cfg,
|
||||
attemptsPath,
|
||||
kto: sessionId ?? null,
|
||||
rodit: () => {
|
||||
// detached: false — работник обязан быть ребёнком НАДЗИРАТЕЛЯ. Отвяжи его,
|
||||
// и надзиратель перестанет видеть и его поток, и его смерть, а значит перестанет
|
||||
// исполнять пределы. Отвязанным живёт надзиратель, не работник.
|
||||
const child = spawn(launch.command, args, {
|
||||
cwd,
|
||||
detached: false,
|
||||
windowsHide: true,
|
||||
shell: launch.shell,
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
return child; // сырой ребёнок: поток читает надзиратель, он же пишет замеры и итог
|
||||
},
|
||||
});
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
return child; // сырой ребёнок: поток читает надзиратель, он же пишет замеры и итог
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
resolveClaudeBin, claudeLaunch, buildArgs, readResult, isRealSuccess, parseStreamLine,
|
||||
isStepBoundary, rateLimitFromEvent, workerEnv, WORKER_ENV_KEYS,
|
||||
} from './spawn.mjs';
|
||||
import { startWorker } from './spawn.mjs';
|
||||
import { resolveBin, needsShell } from './paths.mjs';
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { existsSync, readdirSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { PREDEL_ZHIVOGO_ZAPUSKA, PREDEL_SAMOY_PROGRAMME } from './predel-zhivogo-zapuska.mjs';
|
||||
import { recordSpawnAttempt, readSpawnAttempts } from './schyot-zamerov.mjs';
|
||||
|
||||
// 🔴 ПРЕДЕЛ ВРЕМЕНИ ВСЕМУ ФАЙЛУ. Здесь вправду запускаются `claude --version` и живые
|
||||
// процессы `node`. При пределе по умолчанию 5000 мс это краснело от соседей, а не
|
||||
@@ -326,3 +328,81 @@ describe('имя своей базы доходит до ЖИВОГО проце
|
||||
})).toThrow(/имя своей базы/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('🔴 рождение работника идёт ЧЕРЕЗ СЧЁТ владельца, а не мимо него', () => {
|
||||
// 🔴🔴 ЗДЕСЬ НЕ РОЖДАЕТСЯ НИ ОДНОГО НАСТОЯЩЕГО РАБОТНИКА, и держится это не на обещании,
|
||||
// а на устройстве: `cwd` во всех проверках ниже указывает в НЕСУЩЕСТВУЮЩИЙ каталог.
|
||||
// Приём взят у соседней проверки выше (про имя базы) и по той же причине: с таким `cwd`
|
||||
// система отказывает ДО создания процесса (ENOENT на рабочий каталог), то есть живой
|
||||
// `claude` не заводится ни при каком исходе — ни когда счёт отказал, ни когда пропустил.
|
||||
// Замерено прямо здесь, отдельным запуском: `spawn` возвращает объект, а ошибка приходит
|
||||
// событием — процесса при этом не создаётся вовсе.
|
||||
//
|
||||
// 🔴 Счёт подаётся СВОИМ, временным (`attemptsPath`), а настройки — своими (`cfg`).
|
||||
// Без этого проверка писала бы в настоящее хозяйство `.night`, а его на рабочий угол
|
||||
// один: соседний прибор проверок затирал бы нам счёт посреди опыта.
|
||||
let dir;
|
||||
let schyot;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'rozhdenie-rabotnika-'));
|
||||
schyot = join(dir, 'worker-spawns.jsonl');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const dovody = {
|
||||
cwd: join(process.cwd(), 'net-takogo-kataloga-i-ne-budet'),
|
||||
prompt: 'поручение',
|
||||
settingsPath: 'night-settings.json',
|
||||
sessionId: '00000000-0000-4000-8000-000000000000',
|
||||
dbName: 'night_r_1_w_1',
|
||||
model: 'sonnet',
|
||||
};
|
||||
|
||||
it('🔴 на исчерпанном пределе работник НЕ рождается, и отказ — ЧЕЛОВЕЧЕСКАЯ строка', () => {
|
||||
// 🔴 Вырежи счёт из `startWorker` — и здесь не будет никакого броска вовсе: рождение
|
||||
// пойдёт как шло, предел владельца не сторожит ничего. Проверено вырезанием.
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < 3; i += 1) recordSpawnAttempt(schyot, { at: now - 1000 + i, kto: 'w-старый' });
|
||||
let beda = null;
|
||||
try {
|
||||
startWorker({ ...dovody, cfg: { spawn_budget_per_shift: 3 }, attemptsPath: schyot });
|
||||
} catch (err) {
|
||||
beda = err;
|
||||
}
|
||||
expect(beda, 'работник родился при исчерпанном пределе — предел не сторожит рубли').not.toBeNull();
|
||||
expect(beda.message).toContain('spawn_budget_per_shift');
|
||||
expect(beda.message).toContain('night.config.json');
|
||||
expect(beda.message).toContain(schyot);
|
||||
expect(beda.message, 'отказ похож на след стека, а не на слова для владельца').toMatch(/Что делать/);
|
||||
// Лишней строки в счёт не легло: рождения не было.
|
||||
expect(readSpawnAttempts(schyot)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('🔴 попытка пишется ДО запуска: даже провалившееся рождение съедает счёт', () => {
|
||||
// 🪤 Самый тихий обход: считай мы удачные рождения, сломанный запуск жёг бы деньги
|
||||
// владельца сколько угодно раз при счётчике на нуле. Здесь запуск ВСЕГДА проваливается
|
||||
// (каталога нет), и строка в счёте обязана появиться всё равно.
|
||||
const child = startWorker({ ...dovody, cfg: { spawn_budget_per_shift: 30 }, attemptsPath: schyot });
|
||||
// 🪤 Ошибка запуска придёт событием; без слушателя она убила бы весь прибор проверок.
|
||||
child?.on?.('error', () => {});
|
||||
expect(readSpawnAttempts(schyot), 'запуск был, а счёт остался на нуле').toHaveLength(1);
|
||||
});
|
||||
|
||||
it('🔴 отказ по ненаписанным настройкам счёта НЕ съедает: их описка денег не стоит', () => {
|
||||
// Голову не сказали — бросок про `worker_model` случается ДО счёта, и правильно:
|
||||
// красть у владельца рождение, которого не было, нельзя.
|
||||
expect(() => startWorker({
|
||||
...dovody, model: undefined, cfg: { spawn_budget_per_shift: 30 }, attemptsPath: schyot,
|
||||
})).toThrow(/worker_model/i);
|
||||
expect(readSpawnAttempts(schyot)).toHaveLength(0);
|
||||
|
||||
expect(() => startWorker({
|
||||
...dovody, dbName: null, cfg: { spawn_budget_per_shift: 30 }, attemptsPath: schyot,
|
||||
})).toThrow(/имя своей базы/i);
|
||||
expect(readSpawnAttempts(schyot)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user