diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7526e1e..210786a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node }} cache: npm @@ -79,7 +79,7 @@ jobs: with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' cache: npm @@ -105,7 +105,7 @@ jobs: with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' cache: npm @@ -124,7 +124,7 @@ jobs: with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' cache: npm @@ -132,7 +132,7 @@ jobs: - run: npm run build - id: name run: echo "artifact=package-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ steps.name.outputs.artifact }} path: dist/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc03ae3..1b9ca2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,12 +109,12 @@ jobs: # The publishing tools come from this trusted main workflow, not an artifact. ref: ${{ github.sha }} persist-credentials: false - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' registry-url: https://registry.npmjs.org package-manager-cache: false - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ needs.verify.outputs.artifact }} path: dist diff --git a/package-lock.json b/package-lock.json index 6e11a4d..6c086ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "devDependencies": { "@earendil-works/pi-coding-agent": "0.85.1", - "@types/node": "22.20.2", - "typebox": "1.3.31", + "@types/node": "22.20.5", + "typebox": "1.3.34", "typescript": "7.0.2", "yaml": "2.9.1" }, @@ -28,7 +28,6 @@ "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.85.1.tgz", "integrity": "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ==", "dev": true, - "hasShrinkwrap": true, "license": "MIT", "dependencies": { "@earendil-works/chord": "^0.85.1", @@ -1499,16 +1498,6 @@ "node": ">= 14" } }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1547,19 +1536,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2319,16 +2295,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", - "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2417,9 +2383,9 @@ } }, "node_modules/@types/node": { - "version": "22.20.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", - "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", "dev": true, "license": "MIT", "dependencies": { @@ -2766,10 +2732,33 @@ "node": ">=16.20.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/typebox": { - "version": "1.3.31", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.31.tgz", - "integrity": "sha512-7++UxOb7lAJbJ8pbhN2Yl/ljadShbHZ6GQwst/YEUAlVh1nP8LqYpC2t+R1VysunFpmDffDzhZRsvGchx0PqUA==", + "version": "1.3.34", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.34.tgz", + "integrity": "sha512-wbnzrXXDW8xEFHDZZs2jo1MkhaYlKAY4FRhpBc1+2LF1fZVBGCXGdLEhA/Z/NBbgzJMFfeM8m7elKPa/+KxaUQ==", "dev": true, "license": "MIT" }, @@ -2808,6 +2797,16 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, + "node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 6c2511c..678f06d 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,13 @@ }, "devDependencies": { "@earendil-works/pi-coding-agent": "0.85.1", - "@types/node": "22.20.2", - "typebox": "1.3.31", + "@types/node": "22.20.5", + "typebox": "1.3.34", "typescript": "7.0.2", "yaml": "2.9.1" + }, + "overrides": { + "brace-expansion": "^5.0.12", + "undici": "^8.10.2" } } diff --git a/src/child-process.test.ts b/src/child-process.test.ts index 887f5cb..05f3b08 100644 --- a/src/child-process.test.ts +++ b/src/child-process.test.ts @@ -14,8 +14,21 @@ describe("isSubagentProcess", () => { ); }); + test("returns true when PI_SUBAGENT_CHILD is set to 1", () => { + assert.equal(isSubagentProcess({ PI_SUBAGENT_CHILD: "1" }), true); + }); + + test("returns false when PI_SUBAGENT_CHILD is set to 0", () => { + assert.equal(isSubagentProcess({ PI_SUBAGENT_CHILD: "0" }), false); + }); + + test("returns true when PI_SUBAGENT_RUNNER_PID is set", () => { + assert.equal(isSubagentProcess({ PI_SUBAGENT_RUNNER_PID: "4321" }), true); + }); + test("returns false when the subagent env var is an empty string", () => { assert.equal(isSubagentProcess({ PI_SUBAGENT_AGENT_ID: "" }), false); + assert.equal(isSubagentProcess({ PI_SUBAGENT_RUNNER_PID: "" }), false); }); test("ignores unrelated env vars", () => { diff --git a/src/child-process.ts b/src/child-process.ts index 56f1f5f..bdb714a 100644 --- a/src/child-process.ts +++ b/src/child-process.ts @@ -1,8 +1,19 @@ -/** Environment variable set by run-subagent on child agent processes. */ +/** + * Environment variables set on child agent processes: + * - PI_SUBAGENT_AGENT_ID: Legacy runner identification. + * - PI_SUBAGENT_CHILD: Modern pi-subagents standard (set to "1"). + * - PI_SUBAGENT_RUNNER_PID: Detached background runner process. + */ const SUBAGENT_AGENT_ID_ENV = "PI_SUBAGENT_AGENT_ID"; +const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD"; +const SUBAGENT_RUNNER_PID_ENV = "PI_SUBAGENT_RUNNER_PID"; /** Returns true when the current process is a child subagent process. */ export function isSubagentProcess(env: NodeJS.ProcessEnv): boolean { const agentId = env[SUBAGENT_AGENT_ID_ENV]; - return agentId !== undefined && agentId !== ""; + if (agentId !== undefined && agentId !== "") return true; + if (env[SUBAGENT_CHILD_ENV] === "1") return true; + const runnerPid = env[SUBAGENT_RUNNER_PID_ENV]; + if (runnerPid !== undefined && runnerPid !== "") return true; + return false; } diff --git a/src/index.test.ts b/src/index.test.ts index 0fa4e70..cb19bd1 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -29,6 +29,21 @@ test('factory has no filesystem writes; five relevant hooks, no approval/signal test('subagent factory registers nothing and never creates state',()=>fixture(async({stateDir,hooks})=>{ assert.equal(hooks.size,0);assert.equal(existsSync(stateDir),false); },undefined,{PI_SUBAGENT_AGENT_ID:'child'})); +test('subagent factory registers nothing when PI_SUBAGENT_CHILD or PI_SUBAGENT_RUNNER_PID is set',async()=>{ + await fixture(async({hooks})=>{ assert.equal(hooks.size,0); },undefined,{PI_SUBAGENT_CHILD:'1'}); + await fixture(async({hooks})=>{ assert.equal(hooks.size,0); },undefined,{PI_SUBAGENT_RUNNER_PID:'9999'}); +}); +test('before_agent_start handles missing prompt, custom messages and content-block arrays safely',()=>fixture(async({call})=>{ + await call('session_compact',compact()); + // Custom message without prompt property + assert.equal(await call('before_agent_start',{messageType:'custom',message:{role:'user',content:'test'},systemPrompt:'Base'}),undefined); + // Undefined / empty prompt + assert.equal(await call('before_agent_start',{prompt:'',systemPrompt:'Base'}),undefined); + assert.equal(await call('before_agent_start',{systemPrompt:'Base'}),undefined); + // Array content blocks + const arrayResult = await call('before_agent_start',{prompt:[{type:'text',text:'Database port'}],systemPrompt:'Base prompt'}); + assert.match(arrayResult.systemPrompt,/5432/); +})); test('compaction automatically persists and recall follows the topic across directories and sessions',()=>fixture(async({stateDir,call,ctx})=>{ await call('session_compact',compact()); const result=await call('before_agent_start',{prompt:'Database port',systemPrompt:'Base prompt'}); diff --git a/src/index.ts b/src/index.ts index e7a5be3..c8e0d7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -168,8 +168,13 @@ export default async function memoryEvolution(pi: ExtensionAPI, dependencies: Me if (captured) void enqueue(id, ctx); })); pi.on("before_agent_start", guard((event, ctx) => { + const rawPrompt = 'prompt' in event ? event.prompt : undefined; + const promptText = typeof rawPrompt === 'string' ? rawPrompt + : Array.isArray(rawPrompt) ? (rawPrompt as { type?: string; text?: string }[]).filter(b => b?.type === 'text' && typeof b?.text === 'string').map(b => b.text).join('\n') + : ''; + if (!promptText.trim()) return; lastRecall = 'Last automatic recall failed before completing; see /memory status.'; - const query = resolveRecallQuery(event.prompt, recentUserMessages(ctx)); + const query = resolveRecallQuery(promptText, recentUserMessages(ctx)); const { selected, diagnostics } = retrieveMemories(query.query ? getStore().readMemories() : [], query); const digest = buildRuntimeDigest(selected, query); lastRecall = diagnosticText({ ...diagnostics, injected: digest?.split('\n').filter(line => line.startsWith('{')).length ?? 0,