total-recall-brain 3.43.3 → 3.45.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/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +16 -9
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +3 -3
- package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
- package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
- package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
- package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +17 -16
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +160 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +101 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +54 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +64 -15
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +6 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +28 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +597 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/connect.mjs +2 -29
- package/src/cli/context.mjs +1 -0
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/ingest-okf.mjs +4 -4
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/release.mjs +4 -1
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +10 -3
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/automatic-brain-updates.mjs +11 -6
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +42 -12
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compactor.mjs +7 -2
- package/src/core/context-compiler.mjs +26 -1
- package/src/core/context-policy.mjs +7 -3
- package/src/core/daemon-control.mjs +22 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/global-brain-updates.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/mesh-brain-updates.mjs +2 -1
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-restart.mjs +128 -7
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +33 -6
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +68 -57
- package/src/core/task-context.mjs +9 -8
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +5 -0
- package/src/core/vault-files.mjs +8 -2
- package/src/core/vault.mjs +1 -1
- package/src/core/vector-field.mjs +134 -192
- package/src/server/index.mjs +3 -1
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/** One-shot harness sessions. Portable: this module uses Node built-ins only. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
8
|
+
|
|
9
|
+
// Match the one-shot adapters in worker.mjs. Callers can supply their prepared
|
|
10
|
+
// args (model, permissions and provider settings), without the prompt operand.
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
agy: ['--mode', 'accept-edits', '--sandbox', '--output-format', 'json', '-p'],
|
|
13
|
+
cursor: ['--print', '--output-format', 'json'],
|
|
14
|
+
dsh: ['--profile', 'headless', '--json', '-'],
|
|
15
|
+
grok: ['--output-format', 'json', '-p'],
|
|
16
|
+
muse: ['exec', '--json'],
|
|
17
|
+
pi: ['--print', '--mode', 'json', '--provider', 'openrouter', '--tools', 'read,bash', '--no-extensions'],
|
|
18
|
+
hermes: ['chat', '--oneshot', '--format', 'stream-json', '--query-file', '-'],
|
|
19
|
+
openclaw: ['agent', '--message'],
|
|
20
|
+
aider: ['--yes', '--message'],
|
|
21
|
+
goose: ['run', '--text'],
|
|
22
|
+
openhands: ['run'],
|
|
23
|
+
cline: ['task', '--json'],
|
|
24
|
+
ollama: ['run'],
|
|
25
|
+
};
|
|
26
|
+
const STDIN = new Set(['dsh', 'pi', 'hermes', 'ollama']);
|
|
27
|
+
const PLAIN = new Set(['ollama', 'aider', 'goose', 'openhands', 'cline']);
|
|
28
|
+
// Verified 2026-11-09. No "latest" selectors: parallel jobs must not share state.
|
|
29
|
+
// antigravity.google/docs/cli/reference/; cursor.com/docs/cli/reference/parameters
|
|
30
|
+
// docs.x.ai/build/cli/reference; dev.meta.ai/docs/muse-code/extending
|
|
31
|
+
// github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/cli.md
|
|
32
|
+
// github.com/NousResearch/hermes-agent/blob/main/website/docs/reference/cli-commands.md
|
|
33
|
+
// docs.openclaw.ai/cli/agent; aider.chat/docs/config/options.html
|
|
34
|
+
// goose-docs.ai/docs/guides/goose-cli-commands/
|
|
35
|
+
// github.com/OpenHands/OpenHands-CLI; github.com/cline/cline/blob/main/docs/cli/cli-reference.mdx
|
|
36
|
+
const SELECTOR = { agy: '--conversation', cursor: '--resume', grok: '--resume',
|
|
37
|
+
muse: '--session-id', pi: '--session', hermes: '--resume', openclaw: '--session-id',
|
|
38
|
+
cline: '--id', openhands: '--resume' };
|
|
39
|
+
const emptyUsage = () => ({ inputTokens: null, outputTokens: null, costUsd: null });
|
|
40
|
+
const number = (...values) => values.find(value => typeof value === 'number' && Number.isFinite(value) && value >= 0) ?? null;
|
|
41
|
+
const sumUsage = (a, b) => Object.fromEntries(Object.keys(a).map(key => [key,
|
|
42
|
+
a[key] === null && b[key] === null ? null : (a[key] ?? 0) + (b[key] ?? 0)]));
|
|
43
|
+
const usageOf = value => ({
|
|
44
|
+
inputTokens: number(value?.inputTokens, value?.input_tokens, value?.input),
|
|
45
|
+
outputTokens: number(value?.outputTokens, value?.output_tokens, value?.output),
|
|
46
|
+
costUsd: number(value?.costUsd, value?.cost_usd, value?.total_cost_usd, value?.cost?.total),
|
|
47
|
+
});
|
|
48
|
+
const textOf = content => typeof content === 'string' ? content : Array.isArray(content)
|
|
49
|
+
? content.filter(part => part?.type === 'text').map(part => part.text || '').join('\n') : '';
|
|
50
|
+
const errorText = value => typeof value === 'string' ? value : value?.message;
|
|
51
|
+
const failureKind = text => /quota|rate.?limit|resource_exhausted|\b429\b|insufficient credits/i.test(text) ? 'quota'
|
|
52
|
+
: /not logged in|unauthenticated|\b401\b|invalid api key/i.test(text) ? 'auth' : 'agent';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `args` are the existing one-shot command, before prompt injection. Unknown
|
|
56
|
+
* adapters may pass { id, input } as harness. Their controls stay unsupported.
|
|
57
|
+
* Queued replies are returned in done.pendingMessages and job.pendingMessages,
|
|
58
|
+
* so the next attempt can carry them without pretending they were delivered.
|
|
59
|
+
*/
|
|
60
|
+
export function createResumeDriver({ harness, binary, args = [], cwd, env, job,
|
|
61
|
+
options = {}, onEvent = () => {}, onFrame = () => {}, spawnFn = spawn }) {
|
|
62
|
+
for (const key of ['maxTurns', 'maxTokens', 'maxBudgetUsd']) {
|
|
63
|
+
const value = options[key];
|
|
64
|
+
if (value != null && (typeof value !== 'number' || !Number.isFinite(value) || value < 0 ||
|
|
65
|
+
(key !== 'maxBudgetUsd' && !Number.isInteger(value)))) throw new Error(`Invalid ${key}.`);
|
|
66
|
+
}
|
|
67
|
+
const id = typeof harness === 'string' ? harness : harness?.id;
|
|
68
|
+
const inputMode = harness?.input || (STDIN.has(id) ? 'stdin' : id === 'muse' ? 'file' : 'argument');
|
|
69
|
+
const baseArgs = [...(args.length ? args : COMMANDS[id] || [])];
|
|
70
|
+
const knownResume = Boolean(SELECTOR[id] || ['aider', 'goose'].includes(id));
|
|
71
|
+
const canFork = ['grok', 'pi'].includes(id);
|
|
72
|
+
const pendingMessages = Array.isArray(job?.pendingMessages)
|
|
73
|
+
? job.pendingMessages.filter(item => typeof item?.text === 'string').map(item => ({ ...item })) : [];
|
|
74
|
+
let sessionId = options.sessionId || null, active = null, settled = false, closed = false, closeRequested = false;
|
|
75
|
+
let started = false, directory, totalUsage = emptyUsage(), totalTurns = 0, limitError;
|
|
76
|
+
let resolveDone, controls = Promise.resolve(), lastResponse;
|
|
77
|
+
let done = new Promise(resolve => { resolveDone = resolve; });
|
|
78
|
+
const capabilities = { reply: 'unsupported', interrupt: 'metaharness', answer: 'unsupported',
|
|
79
|
+
fork: 'unsupported', goal: 'unsupported', limits: 'metaharness', schema: 'unsupported', review: 'unsupported' };
|
|
80
|
+
const emit = event => { try { onEvent(event); } catch { /* A view cannot stop a job. */ } };
|
|
81
|
+
const refresh = () => {
|
|
82
|
+
const resume = knownResume && Boolean(sessionId);
|
|
83
|
+
capabilities.reply = capabilities.answer = resume ? 'metaharness' : 'unsupported';
|
|
84
|
+
capabilities.fork = resume && canFork ? 'metaharness' : 'unsupported';
|
|
85
|
+
};
|
|
86
|
+
const setSession = value => {
|
|
87
|
+
if (typeof value !== 'string' || !value) return;
|
|
88
|
+
active?.sessionReady?.(value);
|
|
89
|
+
if (value === sessionId) return;
|
|
90
|
+
sessionId = value; refresh();
|
|
91
|
+
emit({ kind: 'session', text: `${id} session ${value}`, sessionId: value, session: value });
|
|
92
|
+
};
|
|
93
|
+
const resultUsage = () => sumUsage(totalUsage, active?.usage || emptyUsage());
|
|
94
|
+
const finish = result => {
|
|
95
|
+
if (settled) return;
|
|
96
|
+
settled = true;
|
|
97
|
+
if (directory) fs.rmSync(directory, { recursive: true, force: true });
|
|
98
|
+
const value = { response: null, structured: null, error: null, failureKind: null, ...result,
|
|
99
|
+
sessionId, usage: resultUsage(), ...(pendingMessages.length ? { pendingMessages: [...pendingMessages] } : {}) };
|
|
100
|
+
lastResponse = value.response;
|
|
101
|
+
emit({ kind: 'finish', text: value.ok ? 'Finished' : value.error, ...value });
|
|
102
|
+
resolveDone(value);
|
|
103
|
+
};
|
|
104
|
+
const serialize = action => {
|
|
105
|
+
const next = controls.then(action);
|
|
106
|
+
controls = next.catch(() => {});
|
|
107
|
+
return next;
|
|
108
|
+
};
|
|
109
|
+
const checkOpen = () => { if (closed || closeRequested || settled) throw new Error('The session is closed.'); };
|
|
110
|
+
const signal = (run, name) => {
|
|
111
|
+
try {
|
|
112
|
+
if (process.platform !== 'win32' && run.child.pid) process.kill(-run.child.pid, name);
|
|
113
|
+
else run.child.kill(name);
|
|
114
|
+
} catch { /* It may have exited. */ }
|
|
115
|
+
};
|
|
116
|
+
const stop = async reason => {
|
|
117
|
+
const run = active;
|
|
118
|
+
if (!run || run.exited) return;
|
|
119
|
+
run.stopReason ||= reason;
|
|
120
|
+
signal(run, 'SIGTERM');
|
|
121
|
+
// Wait for close (all output drained) before a replacement can start.
|
|
122
|
+
// Harnesses write their final result (session id, usage) after SIGTERM; give them time on a busy machine.
|
|
123
|
+
const timer = setTimeout(() => signal(run, 'SIGKILL'), options.stopGraceMs ?? 5000);
|
|
124
|
+
try { await run.exit; } finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
// A descendant can keep running after its parent exits on SIGTERM.
|
|
127
|
+
if (process.platform !== 'win32') signal(run, 'SIGKILL');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const checkLimits = () => {
|
|
131
|
+
const usage = resultUsage();
|
|
132
|
+
const turns = totalTurns + (active?.turns || 0);
|
|
133
|
+
const cap = options.maxTokens != null && (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) >= options.maxTokens ? 'token'
|
|
134
|
+
: options.maxBudgetUsd != null && (usage.costUsd ?? 0) >= options.maxBudgetUsd ? 'budget'
|
|
135
|
+
: options.maxTurns != null && turns >= options.maxTurns ? 'turn' : null;
|
|
136
|
+
if (cap && !limitError) {
|
|
137
|
+
limitError = `Metaharness ${cap} limit reached.`;
|
|
138
|
+
void stop('limit').then(() => finish({ ok: false, error: limitError, failureKind: 'limit' }));
|
|
139
|
+
}
|
|
140
|
+
return Boolean(limitError);
|
|
141
|
+
};
|
|
142
|
+
const frame = (run, value) => {
|
|
143
|
+
onFrame(value);
|
|
144
|
+
if (!value || typeof value !== 'object') return;
|
|
145
|
+
if (Array.isArray(value)) { for (const item of value) frame(run, item); return; }
|
|
146
|
+
run.frames.push(value);
|
|
147
|
+
const agentMeta = value.result?.meta?.agentMeta || value.meta?.agentMeta || value.payload?.meta?.agentMeta;
|
|
148
|
+
setSession(value.session_id || value.sessionId || value.conversation_id || value.conversationId ||
|
|
149
|
+
(value.type === 'session' ? value.id : null) || agentMeta?.sessionId);
|
|
150
|
+
const type = value.type;
|
|
151
|
+
const message = value.message;
|
|
152
|
+
if (['turn_start', 'turn.started'].includes(type)) emit({ kind: 'step-start', text: 'Turn started' });
|
|
153
|
+
if (['tool_use', 'tool_execution_start'].includes(type)) emit({ kind: 'step-start', text: value.name || value.toolName || 'Tool started' });
|
|
154
|
+
if (['tool_result', 'tool_execution_end'].includes(type)) emit({ kind: 'step-end', text: value.name || value.toolName || 'Tool finished' });
|
|
155
|
+
const text = type === 'text' ? value.text : type === 'message_update' ? value.assistantMessageEvent?.delta
|
|
156
|
+
: type === 'message_end' && message?.role === 'assistant' ? textOf(message.content) : null;
|
|
157
|
+
if (text) emit({ kind: 'message', text });
|
|
158
|
+
if (type === 'turn_end') { run.turns += 1; emit({ kind: 'step-end', text: 'Turn finished' }); }
|
|
159
|
+
if (typeof value.num_turns === 'number') run.turns = Math.max(run.turns, value.num_turns);
|
|
160
|
+
const error = errorText(value.error) || (value.is_error ? value.result : null);
|
|
161
|
+
if (error || type === 'error') { run.error = String(error || value.message || 'Agent failed'); emit({ kind: 'error', text: run.error }); }
|
|
162
|
+
if (value.structured_output !== undefined) run.structured = value.structured_output;
|
|
163
|
+
|
|
164
|
+
// Pi usage is cumulative within a message; agent_end repeats completed
|
|
165
|
+
// messages. Do not add both the updates and the final snapshot.
|
|
166
|
+
let usage;
|
|
167
|
+
if (id === 'pi') {
|
|
168
|
+
if (type === 'message_start' && message?.role === 'assistant') run.partialUsage = emptyUsage();
|
|
169
|
+
if (type === 'message_update' && value.usage) run.partialUsage = usageOf(value.usage);
|
|
170
|
+
if (type === 'message_end' && message?.role === 'assistant') {
|
|
171
|
+
run.messageUsage = sumUsage(run.messageUsage, usageOf(message.usage));
|
|
172
|
+
run.partialUsage = emptyUsage();
|
|
173
|
+
}
|
|
174
|
+
if (type === 'agent_end' && Array.isArray(value.messages)) {
|
|
175
|
+
// agent_end covers one low-level run; message_end remains authoritative
|
|
176
|
+
// when it was observed, including across automatic retries.
|
|
177
|
+
if (!run.sawMessageUsage) run.messageUsage = value.messages.filter(m => m.role === 'assistant')
|
|
178
|
+
.reduce((sum, m) => sumUsage(sum, usageOf(m.usage)), emptyUsage());
|
|
179
|
+
}
|
|
180
|
+
if (type === 'message_end' && message?.role === 'assistant') run.sawMessageUsage = true;
|
|
181
|
+
usage = sumUsage(run.messageUsage, run.partialUsage);
|
|
182
|
+
} else if (value.usage || value.tokens || value.total_cost_usd !== undefined || agentMeta?.usage) {
|
|
183
|
+
usage = usageOf(value.usage || value.tokens || agentMeta?.usage);
|
|
184
|
+
usage.costUsd = number(value.total_cost_usd, value.costUsd, usage.costUsd);
|
|
185
|
+
}
|
|
186
|
+
if (usage && Object.values(usage).some(n => n !== null)) {
|
|
187
|
+
run.usage = { ...run.usage, ...Object.fromEntries(Object.entries(usage).filter(([, n]) => n !== null)) };
|
|
188
|
+
emit({ kind: 'usage', text: 'Usage reported', ...resultUsage() });
|
|
189
|
+
}
|
|
190
|
+
checkLimits();
|
|
191
|
+
};
|
|
192
|
+
const queue = (text, command, requestId) => {
|
|
193
|
+
pendingMessages.push({ command, text, ...(requestId === undefined ? {} : { requestId }) });
|
|
194
|
+
if (job && typeof job === 'object') job.pendingMessages = [...pendingMessages];
|
|
195
|
+
const ack = { ack: true, via: 'metaharness', queued: true, reply: 'unsupported' };
|
|
196
|
+
emit({ kind: 'ack', text: 'Queued for the next attempt; this harness cannot resume this session.', command, ...ack });
|
|
197
|
+
return ack;
|
|
198
|
+
};
|
|
199
|
+
const launch = async (text, { resume = false, fork = false } = {}) => {
|
|
200
|
+
checkOpen();
|
|
201
|
+
if (checkLimits()) return;
|
|
202
|
+
directory ||= fs.mkdtempSync(path.join(os.tmpdir(), 'metaharness-resume-'));
|
|
203
|
+
let argv = [...baseArgs], extra = [];
|
|
204
|
+
if (resume) {
|
|
205
|
+
// Remove an earlier selector before setting the exact saved session.
|
|
206
|
+
const selectors = new Set(['--conversation', '--resume', '--session', '--session-id', '--id']);
|
|
207
|
+
argv = argv.filter((arg, i, all) => !selectors.has(arg) && !selectors.has(all[i - 1]));
|
|
208
|
+
if (id === 'aider') extra.push('--restore-chat-history');
|
|
209
|
+
else if (id === 'goose') extra.push('--resume', '--name', sessionId);
|
|
210
|
+
else extra.push(SELECTOR[id], sessionId);
|
|
211
|
+
if (fork) {
|
|
212
|
+
if (id === 'pi') extra = ['--fork', sessionId];
|
|
213
|
+
else extra.push('--fork-session');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (id === 'aider') {
|
|
217
|
+
const history = argv.indexOf('--chat-history-file');
|
|
218
|
+
if (history >= 0) sessionId = argv[history + 1];
|
|
219
|
+
else {
|
|
220
|
+
sessionId ||= path.join(os.tmpdir(), `metaharness-aider-${randomUUID()}.history.md`);
|
|
221
|
+
extra.push('--chat-history-file', sessionId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (id === 'goose' && !resume) { sessionId ||= `metaharness-${randomUUID()}`; extra.push('--name', sessionId); }
|
|
225
|
+
if (['grok', 'openclaw'].includes(id) && !resume && !argv.includes('--session-id')) {
|
|
226
|
+
sessionId ||= randomUUID(); extra.push('--session-id', sessionId);
|
|
227
|
+
}
|
|
228
|
+
refresh();
|
|
229
|
+
// The worker supplies an unfilled prompt option. Keep its operand next to
|
|
230
|
+
// it even when provider or model options were appended to the command.
|
|
231
|
+
const promptOption = ['agy', 'grok'].includes(id) ? '-p'
|
|
232
|
+
: ['openclaw', 'aider'].includes(id) ? '--message' : id === 'goose' ? '--text' : null;
|
|
233
|
+
if (promptOption) argv = argv.filter(arg => arg !== promptOption);
|
|
234
|
+
argv.push(...extra);
|
|
235
|
+
const prompt = [text, ...pendingMessages.splice(0).map(item => item.text)].join('\n\n');
|
|
236
|
+
if (job && typeof job === 'object') job.pendingMessages = [];
|
|
237
|
+
if (inputMode === 'file') {
|
|
238
|
+
const file = path.join(directory, `${randomUUID()}.txt`);
|
|
239
|
+
fs.writeFileSync(file, prompt, { mode: 0o600 }); argv.push('--prompt-file', file);
|
|
240
|
+
} else if (inputMode !== 'stdin') argv.push(...(promptOption ? [promptOption] : []), prompt);
|
|
241
|
+
let child;
|
|
242
|
+
try { child = spawnFn(binary, argv, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
243
|
+
catch (error) { finish({ ok: false, error: error.message, failureKind: 'process' }); throw error; }
|
|
244
|
+
let resolveExit, resolveSession;
|
|
245
|
+
const run = { child, awaitingHeader: ['pi', 'hermes'].includes(id), frames: [], stdout: '', stderr: '', buffer: '', bytes: 0, turns: 0,
|
|
246
|
+
usage: emptyUsage(), messageUsage: emptyUsage(), partialUsage: emptyUsage(),
|
|
247
|
+
exit: new Promise(resolve => { resolveExit = resolve; }),
|
|
248
|
+
session: new Promise(resolve => { resolveSession = resolve; }), sessionReady: value => resolveSession(value) };
|
|
249
|
+
active = run;
|
|
250
|
+
const decoder = new StringDecoder('utf8');
|
|
251
|
+
const line = text => { try { frame(run, JSON.parse(text)); } catch { /* Plain output is valid for some adapters. */ } };
|
|
252
|
+
const collect = chunk => {
|
|
253
|
+
run.bytes += chunk.length;
|
|
254
|
+
if (run.bytes > 32 * 1024 * 1024) { void stop('output-limit'); return; }
|
|
255
|
+
const text = decoder.write(chunk); run.stdout += text; run.buffer += text;
|
|
256
|
+
let end;
|
|
257
|
+
while ((end = run.buffer.indexOf('\n')) >= 0) { line(run.buffer.slice(0, end)); run.buffer = run.buffer.slice(end + 1); }
|
|
258
|
+
};
|
|
259
|
+
child.stdout.on('data', collect);
|
|
260
|
+
child.stderr.on('data', chunk => { run.stderr = (run.stderr + chunk).slice(-8000); });
|
|
261
|
+
child.stdin.on('error', () => {});
|
|
262
|
+
const exit = (code, signalName) => {
|
|
263
|
+
if (run.exited) return;
|
|
264
|
+
run.buffer += decoder.end();
|
|
265
|
+
if (run.buffer.trim()) line(run.buffer);
|
|
266
|
+
// Pretty-printed JSON is not JSONL.
|
|
267
|
+
if (!run.frames.length) { try { frame(run, JSON.parse(run.stdout)); } catch {} }
|
|
268
|
+
run.exited = true;
|
|
269
|
+
if (!run.turns) run.turns = 1;
|
|
270
|
+
checkLimits();
|
|
271
|
+
totalUsage = sumUsage(totalUsage, run.usage); totalTurns += run.turns;
|
|
272
|
+
active = null; resolveSession(null); resolveExit();
|
|
273
|
+
if (limitError) { finish({ ok: false, error: limitError, failureKind: 'limit' }); return; }
|
|
274
|
+
if (['replace', 'interrupt'].includes(run.stopReason)) return;
|
|
275
|
+
if (run.stopReason === 'close') { finish({ ok: false, error: 'Session closed.', failureKind: 'cancelled' }); return; }
|
|
276
|
+
const frames = run.frames, last = frames.at(-1);
|
|
277
|
+
const agentError = run.error || (last?.exit_code ? `Agent exited ${last.exit_code}` : null) ||
|
|
278
|
+
(['FAILED', 'ERROR', 'CANCELED', 'WAITING'].includes(last?.status) ? last.status : null) ||
|
|
279
|
+
(last?.messages?.some(m => ['error', 'aborted'].includes(m.stopReason)) ? 'Agent turn failed' : null);
|
|
280
|
+
let response = last?.result ?? last?.response ?? last?.text;
|
|
281
|
+
if (id === 'pi') response = frames.filter(f => f.type === 'agent_end').flatMap(f => f.messages || [])
|
|
282
|
+
.filter(m => m.role === 'assistant').map(m => textOf(m.content)).join('\n');
|
|
283
|
+
if (id === 'openclaw') response = (last?.result?.payloads || last?.payload?.payloads || last?.payloads || []).map(p => p.text || '').join('\n') || response;
|
|
284
|
+
if (PLAIN.has(id) || id === 'muse' || !COMMANDS[id]) response ||= run.stdout.trim();
|
|
285
|
+
const error = run.spawnError || agentError || (run.stopReason ? `Agent stopped: ${run.stopReason}` : null) ||
|
|
286
|
+
(code !== 0 ? run.stderr.trim() || `Agent process exited ${code ?? signalName}` : null) ||
|
|
287
|
+
(typeof response !== 'string' || !response.trim() ? 'Agent returned no completed result.' : null);
|
|
288
|
+
finish({ ok: !error, response: typeof response === 'string' ? response : null, structured: run.structured ?? null,
|
|
289
|
+
...(options.captureExecution ? { execution: { stdout: run.stdout, stderr: run.stderr, exitCode: code } } : {}),
|
|
290
|
+
error, failureKind: error ? run.spawnError || run.stopReason ? 'process' : failureKind(error) : null });
|
|
291
|
+
};
|
|
292
|
+
child.once('close', exit);
|
|
293
|
+
const live = new Promise((resolve, reject) => {
|
|
294
|
+
child.once('spawn', resolve);
|
|
295
|
+
child.once('error', error => { run.spawnError = error.message; exit(null); reject(error); });
|
|
296
|
+
});
|
|
297
|
+
child.stdin.end(inputMode === 'stdin' ? prompt : '');
|
|
298
|
+
await live;
|
|
299
|
+
// These streams have a documented session header. Wait until the resumed
|
|
300
|
+
// process has read its input before accepting another replacement.
|
|
301
|
+
if (run.awaitingHeader) await run.session;
|
|
302
|
+
run.awaitingHeader = false;
|
|
303
|
+
return run;
|
|
304
|
+
};
|
|
305
|
+
refresh();
|
|
306
|
+
const deliver = (text, command, requestId) => serialize(async () => {
|
|
307
|
+
checkOpen();
|
|
308
|
+
if (typeof text !== 'string' || !text.trim()) throw new Error('A non-empty message is required.');
|
|
309
|
+
if (!knownResume || !started) return queue(text, command, requestId);
|
|
310
|
+
await stop('replace');
|
|
311
|
+
if (settled || limitError) throw new Error(limitError || 'The session has finished.');
|
|
312
|
+
// JSON-only harnesses may report their ID only while shutting down. Read
|
|
313
|
+
// that final output before deciding whether this session can be resumed.
|
|
314
|
+
if (!sessionId) {
|
|
315
|
+
const ack = queue(text, command, requestId);
|
|
316
|
+
finish({ ok: false, error: 'The harness did not report a resumable session ID.', failureKind: 'process' });
|
|
317
|
+
return ack;
|
|
318
|
+
}
|
|
319
|
+
await launch(text, { resume: true });
|
|
320
|
+
const ack = { ack: true, via: 'metaharness' };
|
|
321
|
+
emit({ kind: 'ack', text: 'Resumed with the message.', command, ...ack });
|
|
322
|
+
return ack;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
capabilities,
|
|
326
|
+
start: brief => serialize(async () => {
|
|
327
|
+
checkOpen();
|
|
328
|
+
if (active || started) throw new Error('The session has already started.');
|
|
329
|
+
if (typeof brief !== 'string' || !brief.trim()) throw new Error('A non-empty brief is required.');
|
|
330
|
+
started = true;
|
|
331
|
+
if (options.sessionId && !knownResume) throw new Error('Resume is unsupported for this harness.');
|
|
332
|
+
if (options.forkSession && (!canFork || options.atTurn !== undefined)) throw new Error('Fork is unsupported for this harness or turn.');
|
|
333
|
+
await launch(brief, { resume: Boolean(options.sessionId && knownResume), fork: options.forkSession === true });
|
|
334
|
+
}),
|
|
335
|
+
send: text => deliver(text, 'send'),
|
|
336
|
+
answer: (requestId, value) => deliver(typeof value === 'string' ? value : JSON.stringify(value), 'answer', requestId),
|
|
337
|
+
interrupt: () => {
|
|
338
|
+
// A startup hook can wait before the header. It must remain stoppable
|
|
339
|
+
// while start() is waiting, rather than blocking behind that wait.
|
|
340
|
+
const startup = active?.awaitingHeader ? stop('interrupt') : null;
|
|
341
|
+
return serialize(async () => { checkOpen(); await startup; await stop('interrupt'); emit({ kind: 'ack', text: 'Interrupted.', command: 'interrupt' }); });
|
|
342
|
+
},
|
|
343
|
+
fork: ({ atTurn } = {}) => serialize(async () => {
|
|
344
|
+
checkOpen();
|
|
345
|
+
if (capabilities.fork === 'unsupported' || atTurn !== undefined) throw new Error('Fork is unsupported for this harness or turn.');
|
|
346
|
+
await stop('replace');
|
|
347
|
+
const previous = sessionId;
|
|
348
|
+
const run = await launch('Continue from this session.', { resume: true, fork: true });
|
|
349
|
+
const forked = await run?.session;
|
|
350
|
+
if (!forked || forked === previous) throw new Error('The harness did not report a forked session ID.');
|
|
351
|
+
return { sessionId: forked };
|
|
352
|
+
}),
|
|
353
|
+
close: () => {
|
|
354
|
+
closeRequested = true;
|
|
355
|
+
const startup = active?.awaitingHeader ? stop('close') : null;
|
|
356
|
+
return serialize(async () => {
|
|
357
|
+
if (closed) return;
|
|
358
|
+
closed = true; await startup; await stop('close');
|
|
359
|
+
if (!settled) finish({ ok: false, error: 'Session closed.', failureKind: 'cancelled' });
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
get done() { return done; },
|
|
363
|
+
next: async text => {
|
|
364
|
+
await serialize(async () => {
|
|
365
|
+
if (!settled || closed || closeRequested) throw new Error('The session is not ready for another turn.');
|
|
366
|
+
settled = false;
|
|
367
|
+
done = new Promise(resolve => { resolveDone = resolve; });
|
|
368
|
+
await launch(knownResume && sessionId ? text : `Previous result:\n${lastResponse}\n\n${text}`, { resume: Boolean(knownResume && sessionId) });
|
|
369
|
+
});
|
|
370
|
+
return done;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/** Job lifecycle around a native session. All dependencies ship with the worker. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { createClaudeDriver } from './claude.mjs';
|
|
6
|
+
import { createCodexDriver } from './codex.mjs';
|
|
7
|
+
import { createResumeDriver } from './resume.mjs';
|
|
8
|
+
import { GOAL_SCHEMA, readResult } from './result.mjs';
|
|
9
|
+
import { tailInbox, appendEvent, waitingRequests } from '../control.mjs';
|
|
10
|
+
import { mcpConfigFor, createMcpTools, NO_ANSWER, QUESTION_TIMEOUT_MS } from '../mcp-server.mjs';
|
|
11
|
+
|
|
12
|
+
// A routed attempt or coordination stage shares its CLI job's limits. Each
|
|
13
|
+
// driver reports its own usage; the job sums those reports without double count.
|
|
14
|
+
const budgets = new Map();
|
|
15
|
+
|
|
16
|
+
export function sessionOptions(request) {
|
|
17
|
+
const result = {};
|
|
18
|
+
if (request.sessionId) result.resumeSession = true;
|
|
19
|
+
for (const key of ['goal', 'maxTurns', 'maxTokens', 'maxBudgetUsd', 'resultSchema', 'approvalPolicy', 'questionTimeoutMs', 'sessionId', 'forkSession', 'atTurn', 'name', 'effort', 'fallbackModel', 'reviewTarget']) {
|
|
20
|
+
if (request[key] !== undefined) result[key] = request[key];
|
|
21
|
+
}
|
|
22
|
+
for (const key of ['maxTurns', 'maxTokens', 'questionTimeoutMs']) if (result[key] !== undefined && (!Number.isSafeInteger(result[key]) || result[key] < 1)) throw new Error(`${key} must be a positive integer.`);
|
|
23
|
+
if (result.maxBudgetUsd !== undefined && (!Number.isFinite(result.maxBudgetUsd) || result.maxBudgetUsd < 0)) throw new Error('maxBudgetUsd must be nonnegative.');
|
|
24
|
+
if (result.goal !== undefined && (typeof result.goal !== 'string' || !result.goal.trim() || result.goal.length > 4000)) throw new Error('goal must have 1..4000 characters.');
|
|
25
|
+
if (result.approvalPolicy !== undefined && !['auto', 'operator', 'rules'].includes(result.approvalPolicy)) throw new Error('Approval policy must be auto, operator, or rules.');
|
|
26
|
+
if (typeof result.resultSchema === 'string') result.resultSchema = JSON.parse(fs.readFileSync(path.resolve(request.cwd, result.resultSchema), 'utf8'));
|
|
27
|
+
if (result.resultSchema !== undefined && typeof result.resultSchema !== 'boolean' && (!result.resultSchema || typeof result.resultSchema !== 'object' || Array.isArray(result.resultSchema))) throw new Error('resultSchema must be a JSON Schema.');
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runSession({ request, adapter, binary, args, env, prompt, progress, register = () => () => {},
|
|
32
|
+
driverFactory, controlToolsFactory = createMcpTools, onFrame = () => {} } = {}) {
|
|
33
|
+
const options = sessionOptions(request);
|
|
34
|
+
options.captureExecution = adapter.id === 'openclaw';
|
|
35
|
+
options.trackTurns = true;
|
|
36
|
+
let goal = options.goal || request.contextTask || request.prompt;
|
|
37
|
+
options.goal = goal.slice(0, 4000);
|
|
38
|
+
const job = request.jobId || process.env.TR_METAHARNESS_JOB || `session-${randomUUID()}`;
|
|
39
|
+
const budget = budgets.get(job) || new Map();
|
|
40
|
+
budgets.set(job, budget);
|
|
41
|
+
const runId = randomUUID();
|
|
42
|
+
const config = mcpConfigFor(job, options);
|
|
43
|
+
if (adapter.id === 'claude') options.mcpConfig = config.claude;
|
|
44
|
+
if (adapter.id === 'codex') {
|
|
45
|
+
// exec flags belong to the old one-shot surface, not app-server.
|
|
46
|
+
const start = args.indexOf('exec');
|
|
47
|
+
const prefix = start < 0 ? [] : args.slice(0, start);
|
|
48
|
+
const model = args.lastIndexOf('--model');
|
|
49
|
+
args = [...prefix, ...(model >= 0 ? ['-c', `model=${JSON.stringify(args[model + 1])}`] : []), ...config.codex];
|
|
50
|
+
}
|
|
51
|
+
let driver, tools, watcher, deferredWatcher, replyWaiter = null, timer, stopResult, stopTask, turns = 0;
|
|
52
|
+
const deferredReplies = new Map();
|
|
53
|
+
let resolveStop;
|
|
54
|
+
const stopped = new Promise(resolve => { resolveStop = resolve; });
|
|
55
|
+
const usage = { inputTokens: null, outputTokens: null, costUsd: null };
|
|
56
|
+
const totals = () => [...budget.values()].reduce((sum, entry) => ({
|
|
57
|
+
inputTokens: sum.inputTokens + (entry.usage.inputTokens || 0), outputTokens: sum.outputTokens + (entry.usage.outputTokens || 0),
|
|
58
|
+
costUsd: sum.costUsd + (entry.usage.costUsd || 0), turns: sum.turns + entry.turns,
|
|
59
|
+
}), { inputTokens: 0, outputTokens: 0, costUsd: 0, turns: 0 });
|
|
60
|
+
const toolResults = [];
|
|
61
|
+
const emit = event => progress ? progress.event(event) : appendEvent(job, event);
|
|
62
|
+
const stop = (failureKind, error) => {
|
|
63
|
+
if (stopResult) return;
|
|
64
|
+
stopResult = { ok: false, failureKind, error };
|
|
65
|
+
resolveStop(stopResult);
|
|
66
|
+
stopTask = Promise.resolve(driver?.interrupt()).catch(() => {});
|
|
67
|
+
};
|
|
68
|
+
budget.set(runId, { usage, turns: 0, stop });
|
|
69
|
+
const limit = error => { for (const entry of budget.values()) entry.stop?.('limit', error); };
|
|
70
|
+
const onEvent = event => {
|
|
71
|
+
if (event.kind !== 'usage') emit(['question', 'approval'].includes(event.kind) ? { ...event, kind: 'native-request' } : event);
|
|
72
|
+
if (event.kind === 'usage') {
|
|
73
|
+
for (const key of ['inputTokens', 'outputTokens', 'costUsd']) if (event[key] != null) {
|
|
74
|
+
usage[key] = adapter.id === 'claude' ? (usage[key] || 0) + event[key] : event[key];
|
|
75
|
+
}
|
|
76
|
+
const used = totals();
|
|
77
|
+
emit({ ...event, ...used, limits: { maxTurns: options.maxTurns, maxTokens: options.maxTokens, maxBudgetUsd: options.maxBudgetUsd } });
|
|
78
|
+
if (options.maxTokens !== undefined && used.inputTokens + used.outputTokens >= options.maxTokens) limit('Job token limit reached.');
|
|
79
|
+
if (options.maxBudgetUsd !== undefined && usage.costUsd !== null && used.costUsd >= options.maxBudgetUsd) limit('Job budget limit reached.');
|
|
80
|
+
if (options.maxBudgetUsd !== undefined && usage.costUsd === null && adapter.id !== 'claude') stop('limit', 'The harness did not report cost, so the USD cap cannot be enforced.');
|
|
81
|
+
}
|
|
82
|
+
if (event.kind === 'finish') {
|
|
83
|
+
turns += event.turns || 1;
|
|
84
|
+
budget.get(runId).turns = turns;
|
|
85
|
+
if (options.maxTurns !== undefined && totals().turns > options.maxTurns) limit('Job turn limit reached.');
|
|
86
|
+
}
|
|
87
|
+
const item = event.item;
|
|
88
|
+
if (event.kind === 'step-end' && toolResults.length < 20 && (event.toolUseId || item?.type === 'commandExecution')) {
|
|
89
|
+
toolResults.push({ tool: item ? 'shell' : 'tool', status: event.isError || item?.exitCode ? 'failed' : 'completed',
|
|
90
|
+
...(Number.isInteger(item?.exitCode) ? { exitCode: item.exitCode } : {}), output: String(item?.aggregatedOutput || event.text || '').slice(0, 4096) });
|
|
91
|
+
}
|
|
92
|
+
if (event.kind === 'question' || event.kind === 'approval') {
|
|
93
|
+
// Native requests use the same policy, timeout and operator path as MCP.
|
|
94
|
+
void Promise.resolve().then(async () => {
|
|
95
|
+
if (!tools) return;
|
|
96
|
+
const answer = await tools.call(event.kind === 'question' ? 'ask' : 'approve', event.kind === 'question'
|
|
97
|
+
? { questions: event.questions || event.request?.questions || [event.text || 'Reply to the agent.'] }
|
|
98
|
+
: { tool_name: event.request?.tool_name || event.request?.command || event.method || 'tool', input: event.request || {} },
|
|
99
|
+
// A native request pauses the harness itself with no model call, so it waits for the answer here.
|
|
100
|
+
{ defer: false });
|
|
101
|
+
let value = answer.content[0].text;
|
|
102
|
+
if (event.kind === 'approval') {
|
|
103
|
+
const approval = JSON.parse(value);
|
|
104
|
+
value = adapter.id === 'claude' ? approval : approval.behavior === 'allow' ? 'allow' : 'deny';
|
|
105
|
+
}
|
|
106
|
+
await driver.answer(event.requestId, value);
|
|
107
|
+
emit({ kind: 'ack', requestId: event.requestId, ok: true, text: 'Native request answered.' });
|
|
108
|
+
}).catch(error => emit({ kind: 'error', text: error.message }));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const factory = driverFactory || (adapter.id === 'claude' ? createClaudeDriver : adapter.id === 'codex' ? createCodexDriver : createResumeDriver);
|
|
112
|
+
const used = totals();
|
|
113
|
+
const driverOptions = { ...options,
|
|
114
|
+
...(options.maxTurns !== undefined ? { maxTurns: Math.max(1, options.maxTurns - used.turns) } : {}),
|
|
115
|
+
...(options.maxBudgetUsd !== undefined ? { maxBudgetUsd: Math.max(0, options.maxBudgetUsd - used.costUsd) } : {}),
|
|
116
|
+
...(options.maxTokens !== undefined ? { maxTokens: Math.max(1, options.maxTokens - used.inputTokens - used.outputTokens) } : {}) };
|
|
117
|
+
driver = factory({ harness: adapter, binary, args, cwd: request.cwd,
|
|
118
|
+
env: { ...env, TR_METAHARNESS_JOB: job }, job: { id: job }, options: driverOptions, onEvent, onFrame });
|
|
119
|
+
tools = controlToolsFactory({ job, approvalPolicy: options.approvalPolicy || 'auto', questionTimeoutMs: options.questionTimeoutMs });
|
|
120
|
+
const unregister = register(() => { stop('cancelled', 'Job cancelled.'); void driver.close(); });
|
|
121
|
+
const wait = promise => Promise.race([promise, stopped]);
|
|
122
|
+
const capabilities = () => ({ ...driver.capabilities,
|
|
123
|
+
goal: driver.capabilities.goal === 'native' ? 'native' : 'metaharness',
|
|
124
|
+
schema: driver.capabilities.schema === 'native' ? 'native' : 'metaharness',
|
|
125
|
+
limits: options.maxTokens !== undefined || adapter.id !== 'claude' ? 'metaharness' : driver.capabilities.limits });
|
|
126
|
+
const next = async (text, schema) => {
|
|
127
|
+
if (options.maxTurns !== undefined && totals().turns >= options.maxTurns) return { ok: false, failureKind: 'limit', error: 'Job turn limit reached.' };
|
|
128
|
+
return wait(driver.next(text, { resultSchema: schema,
|
|
129
|
+
...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns - totals().turns } : {}),
|
|
130
|
+
...(options.maxBudgetUsd !== undefined && usage.costUsd !== null ? { maxBudgetUsd: Math.max(0, options.maxBudgetUsd - totals().costUsd) } : {}) }));
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
timer = setTimeout(() => stop('process', 'Session timeout.'), request.timeoutMs || 900000);
|
|
134
|
+
if (options.maxTurns !== undefined && used.turns >= options.maxTurns || options.maxTokens !== undefined && used.inputTokens + used.outputTokens >= options.maxTokens || options.maxBudgetUsd !== undefined && used.costUsd >= options.maxBudgetUsd) {
|
|
135
|
+
return { ok: false, failureKind: 'limit', error: 'Job limit reached before launch.', usage, turns };
|
|
136
|
+
}
|
|
137
|
+
if (options.maxBudgetUsd === 0) return { ok: false, failureKind: 'limit', error: 'Job budget limit reached before launch.', usage, turns };
|
|
138
|
+
await wait(driver.start(`${prompt}\n\nJob goal: ${goal}`));
|
|
139
|
+
if (stopResult) return stopResult;
|
|
140
|
+
emit({ kind: 'capabilities', text: 'Session controls reported.', capabilities: capabilities() });
|
|
141
|
+
deferredWatcher = tailInbox(job, command => {
|
|
142
|
+
if (command.type !== 'reply' || !command.requestId) return false;
|
|
143
|
+
deferredReplies.set(command.requestId, command.text);
|
|
144
|
+
replyWaiter?.();
|
|
145
|
+
return { via: 'metaharness' };
|
|
146
|
+
}, { recipient: 'deferred' });
|
|
147
|
+
watcher = tailInbox(job, async command => {
|
|
148
|
+
if (command.type === 'reply') return driver.send(command.text);
|
|
149
|
+
if (command.type === 'approve') return driver.answer(command.requestId, command.value);
|
|
150
|
+
if (command.type === 'interrupt') { await driver.interrupt(); stop('interrupted', 'Turn interrupted; the session can be resumed.'); return { via: 'metaharness' }; }
|
|
151
|
+
if (command.type === 'goal') {
|
|
152
|
+
if (driver.setGoal) await driver.setGoal(command.text); else await driver.send(`Job goal: ${command.text}`);
|
|
153
|
+
goal = command.text;
|
|
154
|
+
return { via: driver.setGoal ? 'native' : 'metaharness' };
|
|
155
|
+
}
|
|
156
|
+
if (command.type === 'fork') {
|
|
157
|
+
const fork = await driver.fork({ atTurn: command.atTurn });
|
|
158
|
+
emit({ kind: 'fork', commandId: command.id, ...fork });
|
|
159
|
+
return { via: driver.capabilities.fork };
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
});
|
|
163
|
+
let result = await wait(driver.done);
|
|
164
|
+
// Deferred questions (A-125): the agent ended its turn after asking. Hold the session with no
|
|
165
|
+
// model calls until the operator replies, then start the next turn with the answer.
|
|
166
|
+
while (result.ok && !stopResult) {
|
|
167
|
+
const open = waitingRequests(job).filter(r => r.kind === 'question' && r.source === 'deferred');
|
|
168
|
+
if (!open.length) break;
|
|
169
|
+
emit({ kind: 'waiting', text: `Waiting for the operator on ${open.length} question(s); no model calls until a reply.` });
|
|
170
|
+
const answer = await wait(new Promise(resolve => {
|
|
171
|
+
const check = () => { const hit = open.find(r => deferredReplies.has(r.requestId)); if (hit) { replyWaiter = null; clearTimeout(limit); resolve({ request: hit, text: deferredReplies.get(hit.requestId) }); } };
|
|
172
|
+
const limit = setTimeout(() => { replyWaiter = null; resolve({ request: open[0], text: NO_ANSWER, timeout: true }); }, options.questionTimeoutMs ?? QUESTION_TIMEOUT_MS);
|
|
173
|
+
replyWaiter = check; check();
|
|
174
|
+
}));
|
|
175
|
+
if (!answer || stopResult) break;
|
|
176
|
+
appendEvent(job, { kind: 'ack', command: 'ask', requestId: answer.request.requestId, ok: true, value: answer.text, text: answer.text, ...(answer.timeout ? { timeout: true } : {}) });
|
|
177
|
+
result = await next(`The operator answered your question ${answer.request.text}: ${answer.text}\nContinue the task.`);
|
|
178
|
+
}
|
|
179
|
+
if (result.ok && options.maxTokens !== undefined && usage.inputTokens === null && usage.outputTokens === null) {
|
|
180
|
+
result = { ...result, ok: false, failureKind: 'limit', error: 'The harness did not report token usage, so the token cap cannot be enforced.' };
|
|
181
|
+
}
|
|
182
|
+
if (result.ok && options.maxBudgetUsd !== undefined && usage.costUsd === null) {
|
|
183
|
+
result = { ...result, ok: false, failureKind: 'limit', error: 'The harness did not report cost, so the USD cap cannot be enforced.' };
|
|
184
|
+
}
|
|
185
|
+
if ((result.ok || result.failureKind === 'schema') && options.resultSchema !== undefined) {
|
|
186
|
+
let parsed = readResult(result, options.resultSchema);
|
|
187
|
+
if (parsed.errors.length) {
|
|
188
|
+
const corrected = await next(`Correct your final result once. Return only JSON matching ${JSON.stringify(options.resultSchema)}. Errors: ${parsed.errors.join('; ')}`, options.resultSchema);
|
|
189
|
+
if (!corrected.ok) return { ...corrected, usage: { ...usage }, turns, capabilities: { ...driver.capabilities } };
|
|
190
|
+
result = corrected; parsed = readResult(result, options.resultSchema);
|
|
191
|
+
}
|
|
192
|
+
if (parsed.errors.length) result = { ...result, ok: false, failureKind: 'schema', error: parsed.errors.join('; ') };
|
|
193
|
+
else result.structured = parsed.value;
|
|
194
|
+
}
|
|
195
|
+
const work = result;
|
|
196
|
+
if (result.ok) {
|
|
197
|
+
const check = await next(`Check the goal against what you did. Goal: ${goal}\nReturn only JSON with schema ${JSON.stringify(GOAL_SCHEMA)}. List concrete evidence and all remaining work.`, GOAL_SCHEMA);
|
|
198
|
+
if (!check.ok) result = { ...work, ...check };
|
|
199
|
+
else {
|
|
200
|
+
const parsed = readResult(check, GOAL_SCHEMA);
|
|
201
|
+
result = { ...work, goalCheck: parsed.value, ...(parsed.errors.length ? { ok: false, failureKind: 'goal', error: parsed.errors.join('; ') }
|
|
202
|
+
: !parsed.value.met ? { ok: false, failureKind: 'goal', error: parsed.value.remaining.join('\n') || 'The goal was not met.' }
|
|
203
|
+
: !parsed.value.evidence.length || parsed.value.remaining.length ? { ok: false, failureKind: 'goal', error: 'The goal check needs evidence and no remaining work.' } : {}) };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { ...result, ...stopResult, usage: { ...usage }, turns,
|
|
207
|
+
capabilities: capabilities(),
|
|
208
|
+
toolResults, toolExecutionVerified: toolResults.length > 0, commandExecutionVerified: toolResults.some(t => t.tool === 'shell' && t.status === 'completed') };
|
|
209
|
+
} finally {
|
|
210
|
+
budget.get(runId).stop = null;
|
|
211
|
+
clearTimeout(timer); unregister();
|
|
212
|
+
// Close outstanding questions before waiting for the inbox consumer.
|
|
213
|
+
await tools.close();
|
|
214
|
+
if (stopTask) {
|
|
215
|
+
let grace;
|
|
216
|
+
try { await Promise.race([stopTask, new Promise(resolve => { grace = setTimeout(resolve, 1000); })]); }
|
|
217
|
+
finally { clearTimeout(grace); }
|
|
218
|
+
}
|
|
219
|
+
await driver.close();
|
|
220
|
+
await watcher?.close();
|
|
221
|
+
await deferredWatcher?.close();
|
|
222
|
+
}
|
|
223
|
+
}
|