claude-code-session-manager 0.124.0 → 0.125.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/assets/{AgentLibrary-tsHX4uUD.js → AgentLibrary-CcnnJg8P.js} +1 -1
- package/dist/assets/{Button-BC94OZc0.js → Button-sA4lqAao.js} +1 -1
- package/dist/assets/{DataModel-DX3sReg5.js → DataModel-BSTejIBU.js} +1 -1
- package/dist/assets/{History-DtscQica.js → History-DuwlZsQ5.js} +1 -1
- package/dist/assets/{Hooks-CQERyV2Q.js → Hooks-DHDSoDJ_.js} +1 -1
- package/dist/assets/{Library-DGZ1bv-H.js → Library-XA9osRfi.js} +1 -1
- package/dist/assets/{ListDetail-C6JB9n9x.js → ListDetail-B3goE3Wc.js} +1 -1
- package/dist/assets/{McpServers-C5G8m3j2.js → McpServers-cMZTsPS6.js} +1 -1
- package/dist/assets/{Memory-KgM5KHiQ.js → Memory-D6qntpZ0.js} +1 -1
- package/dist/assets/{Panel-BsOgpYZG.js → Panel-DIyJXHfp.js} +1 -1
- package/dist/assets/{Permissions-DruyM6MH.js → Permissions-DJda4Aii.js} +1 -1
- package/dist/assets/{Plugins-BBWfU2sh.js → Plugins-DcdRVBnU.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CyzjcuYD.js → ProvenanceBadge-BSI_2wAv.js} +1 -1
- package/dist/assets/{SaveBar-D5nZclkV.js → SaveBar-CeL4vXRp.js} +1 -1
- package/dist/assets/{Scheduler-BuxUAv-X.js → Scheduler-CTd19y5U.js} +1 -1
- package/dist/assets/{ScopeSwitcher-BFTsmyXK.js → ScopeSwitcher-BMiHySnz.js} +1 -1
- package/dist/assets/{Settings-CsU2wgPl.js → Settings-D-oIEKZy.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-WTwaKQBk.js → SkillReferenceGraph-B_7WfFjn.js} +1 -1
- package/dist/assets/{Skills-DooJ8MqG.js → Skills-CKpgxhSS.js} +1 -1
- package/dist/assets/{SystemPrompt-DjWMf9Rk.js → SystemPrompt-BjJPTx_a.js} +1 -1
- package/dist/assets/{TagLibrary-DN8yO-eg.js → TagLibrary-DJBB4gL0.js} +1 -1
- package/dist/assets/{TiptapBody-DlbOZsU-.js → TiptapBody-C9Cu2GBL.js} +1 -1
- package/dist/assets/{Toggle-BgPR-zkl.js → Toggle-DbIRTOix.js} +1 -1
- package/dist/assets/{ViewTabs-Bsqxjjs8.js → ViewTabs-BDpHur6l.js} +1 -1
- package/dist/assets/{historyTotals-Dw4jn69k.js → historyTotals-WCJHsBMq.js} +1 -1
- package/dist/assets/{index-DCbAgcJt.js → index-BD0ufZqe.js} +3 -3
- package/dist/assets/{settingsSchema-Duk46cGB.js → settingsSchema-DmjBj3q7.js} +1 -1
- package/dist/assets/{skillFrontmatter-B8F8a18M.js → skillFrontmatter-Tn_XHajE.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-0oP0fhBV.js → useAgentsDirWatch-Vz2iXtsQ.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-web/assets/app.css +32 -0
- package/dist-web/assets/app.js +1479 -0
- package/dist-web/index.html +16 -0
- package/package.json +3 -3
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +28 -10
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +11 -5
- package/src/main/docEdit.cjs +8 -4
- package/src/main/ipcSchemas.cjs +4 -1
- package/src/main/lib/classifyPromptTicket.cjs +9 -4
- package/src/main/lib/claude/runClaudeP.cjs +7 -4
- package/src/main/lib/process/childStdin.cjs +39 -0
- package/src/main/lib/process/childWithLog.cjs +15 -1
- package/src/main/lib/scheduler/launchFailure.cjs +37 -1
- package/src/main/scheduler.cjs +111 -30
- package/src/main/supervisor.cjs +6 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Claude Session Manager</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
+
<script type="module" crossorigin src="./assets/app.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="./assets/app.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body class="bg-bg text-fg font-sans antialiased">
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dev": "concurrently -k -n vite,electron -c magenta,cyan \"npm:dev:renderer\" \"npm:dev:electron\"",
|
|
38
38
|
"dev:renderer": "vite",
|
|
39
39
|
"dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
|
|
40
|
-
"build": "vite build",
|
|
40
|
+
"build": "vite build && vite build --config vite.web.config.ts",
|
|
41
41
|
"build:web": "vite build --config vite.web.config.ts",
|
|
42
42
|
"dist:stage": "npm run build && node scripts/package/stage.cjs",
|
|
43
43
|
"dist:mac": "npm run dist:stage && electron-builder --config electron-builder.yml --mac --publish never",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.main.json",
|
|
48
48
|
"start": "electron .",
|
|
49
49
|
"prepack": "node scripts/write-build-info.cjs",
|
|
50
|
-
"prepublishOnly": "node scripts/write-build-info.cjs &&
|
|
50
|
+
"prepublishOnly": "node scripts/write-build-info.cjs && npm run build",
|
|
51
51
|
"test:unit": "vitest run",
|
|
52
52
|
"test:unit:stability": "bash scripts/test-unit-stability.sh",
|
|
53
53
|
"bench": "node scripts/bench/main-bench.cjs",
|
|
@@ -40,24 +40,37 @@ to ask for reconfirmation once `builder:gate` has passed.
|
|
|
40
40
|
|
|
41
41
|
5. **Create the isolated worktree from that tag:**
|
|
42
42
|
```
|
|
43
|
-
|
|
43
|
+
mkdir -p "$HOME/.cache/sm-publish"
|
|
44
|
+
git worktree add "$HOME/.cache/sm-publish/v<version>" v<version>
|
|
44
45
|
```
|
|
46
|
+
**Never under `/tmp`** (or anywhere under `os.tmpdir()`): the test suite treats such paths as
|
|
47
|
+
test fixtures, so `src/main/__tests__/opsErrorLog.test.cjs` ("a real project root is
|
|
48
|
+
refused") fails inside a `/tmp` worktree.
|
|
45
49
|
|
|
46
|
-
6. **Move into the worktree and
|
|
50
|
+
6. **Move into the worktree, build clean, and run the in-tree gate:**
|
|
47
51
|
```
|
|
48
|
-
cd /
|
|
52
|
+
cd "$HOME/.cache/sm-publish/v<version>"
|
|
49
53
|
npm ci
|
|
50
54
|
npm run typecheck
|
|
55
|
+
npm run test:unit
|
|
51
56
|
```
|
|
52
57
|
`npm ci` gives a fully clean `node_modules` from the lockfile — no leftover local state
|
|
53
|
-
from the dirty main repo can leak into the published artifact.
|
|
58
|
+
from the dirty main repo can leak into the published artifact. Typecheck and unit tests
|
|
59
|
+
must both pass here; stop before `npm publish` on any failure.
|
|
60
|
+
|
|
61
|
+
After the build step (run the project's build, e.g. `npm run build` plus any web build that
|
|
62
|
+
produces `dist-web/`), check that every `files` entry exists:
|
|
63
|
+
```
|
|
64
|
+
test -f dist-web/index.html && test -f dist/index.html
|
|
65
|
+
```
|
|
66
|
+
If either is missing, stop before `npm publish` and report.
|
|
54
67
|
|
|
55
68
|
7. **Publish from inside the worktree:**
|
|
56
69
|
```
|
|
57
70
|
npm publish
|
|
58
71
|
```
|
|
59
72
|
This project's `prepublishOnly` script runs `vite build` — because step 6 already `cd`'d
|
|
60
|
-
into
|
|
73
|
+
into `$HOME/.cache/sm-publish/v<version>`, that build runs **inside the clean worktree**, never
|
|
61
74
|
touching (or being affected by) the live/dirty working directory back in the main repo.
|
|
62
75
|
This is the entire reason the worktree exists: it decouples "what's uncommitted in my
|
|
63
76
|
editor right now" from "what gets built and shipped."
|
|
@@ -81,16 +94,21 @@ to ask for reconfirmation once `builder:gate` has passed.
|
|
|
81
94
|
|
|
82
95
|
9. **Clean up the worktree:**
|
|
83
96
|
```
|
|
84
|
-
git worktree remove /
|
|
97
|
+
git worktree remove "$HOME/.cache/sm-publish/v<version>" --force
|
|
85
98
|
```
|
|
86
99
|
|
|
87
|
-
10. **Verify the publish actually landed on the registry
|
|
100
|
+
10. **Verify the publish actually landed on the registry.** `npm view` is cached and lags for
|
|
101
|
+
minutes after a real publish, so check a cache-busted packument and the tarball first:
|
|
88
102
|
```
|
|
103
|
+
curl -s "https://registry.npmjs.org/<packageName>?t=$(date +%s)" # dist-tags.latest + versions
|
|
104
|
+
curl -sI <tarball URL from versions["<version>"].dist.tarball> # expect HTTP 200
|
|
89
105
|
npm view <packageName> version
|
|
90
106
|
npm view <packageName> dist-tags
|
|
91
107
|
```
|
|
92
|
-
Confirm
|
|
93
|
-
project intentionally publishes to a different dist-tag).
|
|
108
|
+
Confirm `dist-tags.latest` is `<version>`, `<version>` is in `versions`, and the tarball
|
|
109
|
+
returns 200 (unless the project intentionally publishes to a different dist-tag). If the
|
|
110
|
+
packument is current, an unchanged `npm view` for a few minutes is normal lag, not a
|
|
111
|
+
failure — do not republish.
|
|
94
112
|
|
|
95
113
|
11. **Post-publish: verify the installer release.** Pushing `v<version>` (step 8) triggers
|
|
96
114
|
`.github/workflows/release.yml`. Confirm the tag push succeeded, then poll (bounded: at most
|
|
@@ -109,7 +127,7 @@ to ask for reconfirmation once `builder:gate` has passed.
|
|
|
109
127
|
Stop at that step. Do not proceed. Report which step failed and its output. If the worktree
|
|
110
128
|
was already created (step 5+), leave it in place if its state is relevant evidence for
|
|
111
129
|
diagnosing the failure; otherwise remove it (step 9) before reporting, so a retry doesn't
|
|
112
|
-
collide with a stale
|
|
130
|
+
collide with a stale `$HOME/.cache/sm-publish/v<version>` path.
|
|
113
131
|
|
|
114
132
|
## Output
|
|
115
133
|
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.125.0",
|
|
3
|
+
"gitSha": "3b39163b6942d4b5b636b06dc2d737366b1d78e2",
|
|
4
|
+
"gitShortSha": "3b39163b",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-27T02:44:41.942Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -52,6 +52,7 @@ const { resolveEpicEffort, effortArgs } = require('./lib/claude/agentEffortResol
|
|
|
52
52
|
const { planEpicSpawn } = require('./lib/epic/epicSpawnPlan.cjs');
|
|
53
53
|
const epicWorktreeMint = require('./lib/epic/epicWorktreeMint.cjs');
|
|
54
54
|
const logs = require('./logs.cjs');
|
|
55
|
+
const { feedChildStdin } = require('./lib/process/childStdin.cjs');
|
|
55
56
|
const { createHeadTailBuffer } = require('./lib/util/headTailBuffer.cjs');
|
|
56
57
|
|
|
57
58
|
// Caps on per-run stream buffers (a run can last hours).
|
|
@@ -627,7 +628,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
627
628
|
// used for the flag-swap retry so both attempts share one construction path.
|
|
628
629
|
const buildArgs = (resumeFlag) => {
|
|
629
630
|
const a = [
|
|
630
|
-
'-p',
|
|
631
|
+
'-p',
|
|
631
632
|
'--model', model,
|
|
632
633
|
...effortFlags,
|
|
633
634
|
'--dangerously-skip-permissions',
|
|
@@ -712,9 +713,11 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
712
713
|
// One spawn of the CLI. Per-attempt state (buffers, child) lives in here;
|
|
713
714
|
// everything turn-level (timers, latch, lane) is in the enclosing scope.
|
|
714
715
|
const startAttempt = (resumeFlag, attemptNo) => {
|
|
715
|
-
// Spawn with stdin
|
|
716
|
-
//
|
|
717
|
-
//
|
|
716
|
+
// Spawn with a stdin pipe: the prompt is fed on stdin (never argv — E2BIG
|
|
717
|
+
// above 128 KB per argument on Linux, 8191 chars on Windows cmd.exe) and
|
|
718
|
+
// feedChildStdin closes it, satisfying "claude -p stdin must be closed".
|
|
719
|
+
// stdout is piped for real-time NDJSON streaming; stderr piped for
|
|
720
|
+
// error-message capture.
|
|
718
721
|
let thisChild;
|
|
719
722
|
try {
|
|
720
723
|
const chatArgs = buildArgs(resumeFlag);
|
|
@@ -724,7 +727,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
724
727
|
...(target.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
725
728
|
cwd: execCwd,
|
|
726
729
|
env: childEnv,
|
|
727
|
-
stdio: ['
|
|
730
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
728
731
|
...detachedSpawnOpts(), // own process group so killTree can SIGTERM descendants
|
|
729
732
|
});
|
|
730
733
|
} catch (err) {
|
|
@@ -739,6 +742,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
739
742
|
return;
|
|
740
743
|
}
|
|
741
744
|
child = thisChild;
|
|
745
|
+
feedChildStdin(thisChild, fullPrompt, {
|
|
746
|
+
log: (m) => logs.writeLine({ scope: 'chatRunner', level: 'warn', message: m }),
|
|
747
|
+
});
|
|
742
748
|
|
|
743
749
|
// ─── Stream parsing ────────────────────────────────────────────────────
|
|
744
750
|
// stdout is newline-delimited JSON (stream-json format). We buffer partial
|
package/src/main/docEdit.cjs
CHANGED
|
@@ -20,6 +20,7 @@ const { spawn } = require('node:child_process');
|
|
|
20
20
|
const crypto = require('node:crypto');
|
|
21
21
|
const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claude/claudeBin.cjs');
|
|
22
22
|
const { headlessPermissionArgs, ensureCliCapsProbed } = require('./lib/claude/claudeCliCaps.cjs');
|
|
23
|
+
const { feedChildStdin } = require('./lib/process/childStdin.cjs');
|
|
23
24
|
const { withProcRole } = require('./lib/process/cleanEnv.cjs');
|
|
24
25
|
const { extractJson } = require('./lib/util/extractJson.cjs');
|
|
25
26
|
const { assertInsideHome } = require('./lib/insideHome.cjs');
|
|
@@ -94,7 +95,7 @@ async function runClaude(prompt, { model = 'sonnet', timeoutMs = 90_000, systemP
|
|
|
94
95
|
let bin;
|
|
95
96
|
try { bin = resolveClaudeBin(); } catch (e) { resolve({ ok: false, error: `claude not found: ${e?.message}` }); return; }
|
|
96
97
|
const args = [
|
|
97
|
-
'-p',
|
|
98
|
+
'-p',
|
|
98
99
|
'--model', model,
|
|
99
100
|
'--dangerously-skip-permissions',
|
|
100
101
|
...headlessPermissionArgs(),
|
|
@@ -102,11 +103,13 @@ async function runClaude(prompt, { model = 'sonnet', timeoutMs = 90_000, systemP
|
|
|
102
103
|
'--output-format', 'text',
|
|
103
104
|
];
|
|
104
105
|
if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
|
|
105
|
-
//
|
|
106
|
-
//
|
|
106
|
+
// The prompt goes on stdin (argv caps one argument at 128 KB -> E2BIG) and
|
|
107
|
+
// stdin is closed right after the write, never left open — `claude -p`
|
|
108
|
+
// otherwise blocks waiting on it and returns empty. SM_KG_INTERNAL=1 tells the
|
|
107
109
|
// prompt-logging hook to skip this invocation.
|
|
108
110
|
const target = claudeSpawnTarget('aux', 'docedit', bin);
|
|
109
|
-
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['
|
|
111
|
+
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
112
|
+
feedChildStdin(child, prompt);
|
|
110
113
|
let out = '';
|
|
111
114
|
let err = '';
|
|
112
115
|
// Cap accumulated stdout — the model output shouldn't grow `out` without
|
|
@@ -250,6 +253,7 @@ module.exports = {
|
|
|
250
253
|
parseDocEdit,
|
|
251
254
|
editPrompt,
|
|
252
255
|
runDocEdit,
|
|
256
|
+
runClaude,
|
|
253
257
|
docEditViaSession,
|
|
254
258
|
MAX_DOC_CONTEXT,
|
|
255
259
|
truncateDocumentText,
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -252,7 +252,10 @@ const promptSessionsMergeToMain = z.object({
|
|
|
252
252
|
// integrateEpicBranch can apply the same carried-wip-only skip the job
|
|
253
253
|
// path already gets. Absent on a worktree created before this field
|
|
254
254
|
// existed, or one whose base tree was clean at creation.
|
|
255
|
-
|
|
255
|
+
// The renderer sends the Epic's full carried diff (`git diff HEAD --name-only`),
|
|
256
|
+
// which is uncapped; 3,306 paths were seen on 2026-10-26. A tight cap rejects
|
|
257
|
+
// every merge for such an Epic.
|
|
258
|
+
carriedPaths: z.array(z.string().min(1).max(4096)).max(100000).optional(),
|
|
256
259
|
});
|
|
257
260
|
|
|
258
261
|
// Renderer entry points for the per-project "disable Epic worktree
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const { spawn } = require('node:child_process');
|
|
19
19
|
const { resolveClaudeBin, claudeSpawnTarget } = require('./claude/claudeBin.cjs');
|
|
20
20
|
const { cleanChildEnv, pathWithUserBins } = require('./process/cleanEnv.cjs');
|
|
21
|
+
const { feedChildStdin } = require('./process/childStdin.cjs');
|
|
21
22
|
|
|
22
23
|
const CLASSIFY_TIMEOUT_MS = 30_000;
|
|
23
24
|
const CLASSIFY_MODEL = 'sonnet';
|
|
@@ -61,8 +62,10 @@ function classifyPromptTicket(text) {
|
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
const childEnv = cleanChildEnv({ PATH: pathWithUserBins() });
|
|
65
|
+
// The prompt goes on stdin, never argv: a >128 KB argv element fails spawn with E2BIG.
|
|
66
|
+
const prompt = `${CLASSIFY_PROMPT_PREFIX}${text}`;
|
|
64
67
|
const args = [
|
|
65
|
-
'-p',
|
|
68
|
+
'-p',
|
|
66
69
|
'--model', CLASSIFY_MODEL,
|
|
67
70
|
'--strict-mcp-config',
|
|
68
71
|
'--output-format', 'text',
|
|
@@ -78,9 +81,11 @@ function classifyPromptTicket(text) {
|
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
try {
|
|
81
|
-
// stdin
|
|
84
|
+
// stdin is a pipe fed with the prompt and then ended (feedChildStdin), so
|
|
85
|
+
// `claude -p` neither blocks on stdin nor sees an oversized argv element.
|
|
82
86
|
const target = claudeSpawnTarget('aux', 'classify', claudeBin);
|
|
83
|
-
child = spawnImpl(target.command, args, { env: childEnv, stdio: ['
|
|
87
|
+
child = spawnImpl(target.command, args, { env: childEnv, stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
88
|
+
feedChildStdin(child, prompt);
|
|
84
89
|
} catch {
|
|
85
90
|
finish('inline');
|
|
86
91
|
return;
|
|
@@ -115,4 +120,4 @@ function classifyPromptTicket(text) {
|
|
|
115
120
|
});
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
module.exports = { classifyPromptTicket, parseVerdict, CLASSIFY_TIMEOUT_MS, __setSpawnForTest };
|
|
123
|
+
module.exports = { classifyPromptTicket, CLASSIFY_PROMPT_PREFIX, parseVerdict, CLASSIFY_TIMEOUT_MS, __setSpawnForTest };
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* cost-gated synthesis caller (Memory Clusters, the Project Brief) shares one
|
|
8
8
|
* spawn/timeout/output-cap implementation instead of forking it per feature.
|
|
9
9
|
*
|
|
10
|
-
* Pattern:
|
|
11
|
-
*
|
|
10
|
+
* Pattern: the prompt is written to stdin (never argv — Linux caps one
|
|
11
|
+
* argument at 128 KB, E2BIG) and stdin is closed right after the write, never
|
|
12
|
+
* left open (`claude -p` blocks forever waiting on an open pipe), model pinned explicitly by the caller, hard
|
|
12
13
|
* timeout that resolves {ok:false} rather than hanging, SM_KG_INTERNAL=1 so
|
|
13
14
|
* the prompt-logging hook skips these internal calls, and an output-size cap
|
|
14
15
|
* so a runaway response can't grow `out` unbounded.
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
const { spawn } = require('node:child_process');
|
|
18
19
|
const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
|
|
19
20
|
const { headlessPermissionArgs, ensureCliCapsProbed } = require('./claudeCliCaps.cjs');
|
|
21
|
+
const { feedChildStdin } = require('../process/childStdin.cjs');
|
|
20
22
|
const { withProcRole } = require('../process/cleanEnv.cjs');
|
|
21
23
|
|
|
22
24
|
const MAX_OUT_BYTES = 8 * 1024 * 1024;
|
|
@@ -28,7 +30,7 @@ async function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, syste
|
|
|
28
30
|
let bin;
|
|
29
31
|
try { bin = resolveClaudeBin(); } catch (e) { resolve({ ok: false, error: `claude not found: ${e?.message}` }); return; }
|
|
30
32
|
const args = [
|
|
31
|
-
'-p',
|
|
33
|
+
'-p',
|
|
32
34
|
'--model', model,
|
|
33
35
|
'--dangerously-skip-permissions',
|
|
34
36
|
...headlessPermissionArgs(),
|
|
@@ -37,7 +39,8 @@ async function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, syste
|
|
|
37
39
|
];
|
|
38
40
|
if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
|
|
39
41
|
const target = claudeSpawnTarget('aux', 'runclaudep', bin);
|
|
40
|
-
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['
|
|
42
|
+
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
43
|
+
feedChildStdin(child, prompt);
|
|
41
44
|
let out = '';
|
|
42
45
|
let err = '';
|
|
43
46
|
let killedForSize = false;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* childStdin.cjs — write a prompt/text to a child's stdin and close it.
|
|
5
|
+
*
|
|
6
|
+
* Why prompts go on stdin, not argv: Linux caps a single argument at
|
|
7
|
+
* MAX_ARG_STRLEN (131072 bytes) and Windows cmd.exe caps a command line at
|
|
8
|
+
* 8191 characters; a larger argv element fails the spawn with E2BIG
|
|
9
|
+
* (incident 2026-10-26, spawn E2BIG). Ending the pipe also satisfies the rule
|
|
10
|
+
* that `claude -p`'s stdin must be closed (claude CLI 2.1.302 read a 204 KB
|
|
11
|
+
* prompt from stdin and exited 0).
|
|
12
|
+
*
|
|
13
|
+
* The EPIPE handling lives here, in one place: a child that exits without
|
|
14
|
+
* reading raises an 'error' on the pipe; it is logged once and swallowed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {{ stdin?: any } | null | undefined} child
|
|
19
|
+
* @param {string} text
|
|
20
|
+
* @param {{ log?: (msg: string) => void }} [opts]
|
|
21
|
+
* @returns {boolean} true when the text was handed to child.stdin
|
|
22
|
+
*/
|
|
23
|
+
function feedChildStdin(child, text, { log } = {}) {
|
|
24
|
+
if (!child || !child.stdin) return false;
|
|
25
|
+
let logged = false;
|
|
26
|
+
// Listener FIRST: the write can fail asynchronously with EPIPE.
|
|
27
|
+
child.stdin.on('error', (err) => {
|
|
28
|
+
if (logged) return;
|
|
29
|
+
logged = true;
|
|
30
|
+
if (typeof log !== 'function') return;
|
|
31
|
+
try {
|
|
32
|
+
log(`[childStdin] stdin write failed (${(err && (err.code || err.message)) || 'unknown'}): the child exited before reading its input\n`);
|
|
33
|
+
} catch { /* logging must never throw */ }
|
|
34
|
+
});
|
|
35
|
+
child.stdin.end(text, 'utf8');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { feedChildStdin };
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
const fs = require('node:fs');
|
|
54
54
|
const { spawn, execFile } = require('node:child_process');
|
|
55
55
|
const { killTree } = require('./killTree.cjs');
|
|
56
|
+
const { feedChildStdin } = require('./childStdin.cjs');
|
|
56
57
|
|
|
57
58
|
// Injectable for tests (enumeration-failure coverage).
|
|
58
59
|
const deps = { execFile };
|
|
@@ -205,6 +206,16 @@ function sweepChildProcessGroup(child, detached, safeLog) {
|
|
|
205
206
|
.then((leaked) => { kill(); return leaked; });
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Spawn a child with its stdout/stderr on the log fd and supervise it.
|
|
211
|
+
*
|
|
212
|
+
* `spawn.stdinText` (optional string): when given, the child's stdin is a pipe
|
|
213
|
+
* that receives the text (UTF-8) and is then closed; otherwise stdin is
|
|
214
|
+
* 'ignore'. It exists because argv is a poor channel for large input: Linux
|
|
215
|
+
* caps a single argument at MAX_ARG_STRLEN (131072 bytes, spawn E2BIG) and
|
|
216
|
+
* Windows cmd.exe caps a command line at 8191 characters. A stdin error
|
|
217
|
+
* (EPIPE when the child never reads) is logged once and otherwise ignored.
|
|
218
|
+
*/
|
|
208
219
|
function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watchdogs = [], onExit }) {
|
|
209
220
|
const startedAt = Date.now();
|
|
210
221
|
|
|
@@ -296,11 +307,12 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
|
|
|
296
307
|
|
|
297
308
|
// Attempt synchronous spawn. On failure, call handleDone immediately so the
|
|
298
309
|
// caller's onExit can resolve any pending Promise before we return null.
|
|
310
|
+
const stdinText = typeof spawnSpec.stdinText === 'string' ? spawnSpec.stdinText : null;
|
|
299
311
|
let child;
|
|
300
312
|
try {
|
|
301
313
|
child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
302
314
|
...spawnSpec.options,
|
|
303
|
-
stdio: ['ignore', fd, fd],
|
|
315
|
+
stdio: [stdinText !== null ? 'pipe' : 'ignore', fd, fd],
|
|
304
316
|
});
|
|
305
317
|
} catch (e) {
|
|
306
318
|
handleDone(-1, null, e, /* spawnFailed */ true);
|
|
@@ -309,6 +321,8 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
|
|
|
309
321
|
|
|
310
322
|
ctx.child = child;
|
|
311
323
|
|
|
324
|
+
if (stdinText !== null) feedChildStdin(child, stdinText, { log: safeLog });
|
|
325
|
+
|
|
312
326
|
child.on('error', (err) => handleDone(-1, null, err, false));
|
|
313
327
|
child.on('exit', (code, signal) => handleDone(code, signal, undefined, false));
|
|
314
328
|
|
|
@@ -57,6 +57,38 @@ const LAUNCH_FAILURE_KINDS = Object.freeze({
|
|
|
57
57
|
API_ERROR: 'api_error',
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Argv-size guard (incident 2026-10-26, social-signals-trader): an oversized
|
|
62
|
+
* `-p` argument made `spawn` fail with E2BIG (exit -1 in ~86 ms), which
|
|
63
|
+
* became `impl_failed:exit_-1` and spawned a fix-plan that failed the same
|
|
64
|
+
* way. Linux caps ONE argv string at MAX_ARG_STRLEN = 131072 bytes, counting
|
|
65
|
+
* the NUL terminator. The cause is one job's input, not the persona's
|
|
66
|
+
* environment, so this never arms the per-persona launch breaker.
|
|
67
|
+
*/
|
|
68
|
+
const MAX_ARG_BYTES = 131072;
|
|
69
|
+
|
|
70
|
+
/** First argv element whose utf8 bytes + NUL exceed `limit`, else null. */
|
|
71
|
+
function findOversizedArg(args, limit = MAX_ARG_BYTES) {
|
|
72
|
+
if (!Array.isArray(args)) return null;
|
|
73
|
+
for (let index = 0; index < args.length; index++) {
|
|
74
|
+
const a = args[index];
|
|
75
|
+
if (typeof a !== 'string') continue;
|
|
76
|
+
const bytes = Buffer.byteLength(a, 'utf8') + 1;
|
|
77
|
+
if (bytes <= limit) continue;
|
|
78
|
+
let flag = '(positional)';
|
|
79
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
80
|
+
if (typeof args[j] === 'string' && args[j].startsWith('--')) { flag = args[j]; break; }
|
|
81
|
+
}
|
|
82
|
+
return { index, flag, bytes };
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True when a spawn error message is the OS refusing an oversized argv. */
|
|
88
|
+
function isArgvTooLargeSpawnError(message) {
|
|
89
|
+
return /E2BIG|ENAMETOOLONG/.test(String(message ?? ''));
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
/** Tail bytes scanned for the `result` event — same budget classifyRunOutcome uses. */
|
|
61
93
|
const RESULT_TAIL_BYTES = 65536;
|
|
62
94
|
|
|
@@ -310,9 +342,10 @@ function evaluateLaunchGate(block, { now, claudeVersion } = {}) {
|
|
|
310
342
|
* closed set so operators never have to open a transcript to tell a
|
|
311
343
|
* non-start from an implementation failure from a verifier downgrade.
|
|
312
344
|
*/
|
|
313
|
-
function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure, budgetKill }) {
|
|
345
|
+
function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure, budgetKill, launchError }) {
|
|
314
346
|
if (worktreeIntegrationFailure) return 'worktree_integration_failed';
|
|
315
347
|
if (effectiveStatus === 'completed') return 'completed';
|
|
348
|
+
if (launchError === 'argv_too_large') return 'launch_failure:argv_too_large';
|
|
316
349
|
if (budgetKill) return 'budget_exceeded';
|
|
317
350
|
if (sigtermOverride) return 'signal_kill_with_commit';
|
|
318
351
|
if (exitCode === 143 || exitCode === 137) return 'signal_kill';
|
|
@@ -338,6 +371,9 @@ function writeOutcomeSidecar(runDir, slug, outcome) {
|
|
|
338
371
|
}
|
|
339
372
|
|
|
340
373
|
module.exports = {
|
|
374
|
+
MAX_ARG_BYTES,
|
|
375
|
+
findOversizedArg,
|
|
376
|
+
isArgvTooLargeSpawnError,
|
|
341
377
|
LAUNCH_FAILURE_KINDS,
|
|
342
378
|
LAUNCH_BLOCK_MAX_ATTEMPTS,
|
|
343
379
|
LAUNCH_PROBE_STALE_MS,
|