Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -124,15 +124,15 @@ 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
- run: npm ci --ignore-scripts
- 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/
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 41 additions & 42 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
13 changes: 13 additions & 0 deletions src/child-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
15 changes: 13 additions & 2 deletions src/child-process.ts
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down