mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Implement ACPX outbound extension delivery
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4237bc369a
commit
36e7121046
4 files changed
+63
-1
No files matched your search
@@ -42,6 +42,26 @@ prices are estimates. Unpriced usage must stay unpriced in the ledger and UI.
|
||||
|
||||
## Evidence so far
|
||||
|
||||
- At `4237bc369`, the native turn-binding correction advances the steering
|
||||
journey past `steering_stale_turn`, but the provider boundary still rejects
|
||||
delivery. The real patched ACPX client lacks `requestExtension`: its types
|
||||
and runtime callers declare it, while its implementation omits it. A real
|
||||
package regression fails with that exact TypeError before the correction,
|
||||
then passes steering and follow-up during an unanswered prompt afterward.
|
||||
This is a separate runtime correction and needs a fresh installed journey.
|
||||
- The corrected Intel public package at `4237bc369` passes credential-free
|
||||
closed startup in 37.8 seconds. Its CI typecheck, build, native tests and
|
||||
browser shards pass. CI retains two failures: a resumed durability test
|
||||
inherits the deliberately killed attempt's 500 ms timeout; the Linux public
|
||||
Pi probe reaches `session_handshake_timeout`. The former gets an explicit
|
||||
resumed-turn bound. The latter remains an admission blocker; accepting a
|
||||
timeout as successful installation would weaken the release gate.
|
||||
- The local Linux image build at `4237bc369` fails before a provider starts
|
||||
because the committed lock does not match the source patch configuration.
|
||||
The official image workflow already regenerates its private build lock;
|
||||
local builds must do the same and record that resolved lock's digest.
|
||||
No repository lockfile or workflow changes are required.
|
||||
|
||||
- `2b50801b2`: installed restart failed before native answer delivery. The
|
||||
durable request turn ID differs from the provider turn ID. The browser's
|
||||
issue-identifier route also differed from the matcher's UUID route.
|
||||
|
||||
@@ -1813,6 +1813,9 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
sessionId: binding.sessionId,
|
||||
attemptId: "attempt-resumed",
|
||||
resumeOf: "attempt-killed",
|
||||
// Only the killed attempt deliberately uses the short timeout. Allow
|
||||
// durable disk writes to complete during the resumed successful turn.
|
||||
turnTimeoutMs: 5_000,
|
||||
});
|
||||
expect(resumed.snapshot().providerThreadId).toBe(state.threadId);
|
||||
expect(resumed.snapshot().attempts).toMatchObject([
|
||||
|
||||
@@ -25,6 +25,8 @@ readline.createInterface({input:process.stdin}).on('line',(line)=>{
|
||||
send({method:'fixture/ignored',params:{sessionId:'session-1'}});
|
||||
send({method:'fixture/activity',params:{sessionId:'wrong-session'}});
|
||||
send({id:0,method:mode==='unknown'?'fixture/not-enabled':'fixture/question',params:{sessionId:mode==='wrong-session'?'wrong-session':'session-1',value:'private-question'}});
|
||||
} else if(message.method==='pi/steer' || message.method==='pi/follow_up') {
|
||||
send({id:message.id,result:{accepted:true,sessionId:message.params.sessionId,disposition:'queued',message:message.params.message}});
|
||||
} else if(message.method==='session/cancel') { send({id:promptId,result:{stopReason:'cancelled'}}); promptId=undefined; }
|
||||
else if(message.id===0 && !message.method) {
|
||||
send({method:'session/update',params:{sessionId:'session-1',update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text:JSON.stringify(message)}}}});
|
||||
@@ -77,6 +79,29 @@ test("initialize retains mandatory capabilities while merging provider metadata"
|
||||
});
|
||||
});
|
||||
|
||||
test("real patched client sends Pi steering and follow-up while its original prompt awaits input", { timeout: 10000 }, async () => {
|
||||
let observe; const started = new Promise(resolve => { observe = resolve; });
|
||||
await withClient({ onExtensionRequest: async () => { observe(); return await new Promise(() => {}); } }, async client => {
|
||||
const pending = client.prompt("session-1", "question");
|
||||
await started;
|
||||
try {
|
||||
for (const method of ["pi/steer", "pi/follow_up"]) {
|
||||
assert.deepEqual(await client.requestExtension(method, { sessionId: "session-1", message: "Keep the original turn" }), {
|
||||
accepted: true, sessionId: "session-1", disposition: "queued", message: "Keep the original turn",
|
||||
});
|
||||
}
|
||||
for (const method of ["session/prompt", "fs/read_text_file", "initialize", "bad method/name"]) {
|
||||
await assert.rejects(client.requestExtension(method, {}), /extension method/);
|
||||
}
|
||||
await assert.rejects(client.requestExtension("pi/steer", []), /payload must be an object/);
|
||||
await assert.rejects(client.requestExtension("pi/steer", { message: "x".repeat(256 * 1024) }), /bounded size/);
|
||||
} finally {
|
||||
await client.cancel("session-1");
|
||||
assert.equal((await pending).stopReason, "cancelled");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("unknown methods and cross-session requests never reach the host callback", { timeout: 10000 }, async () => {
|
||||
let calls = 0;
|
||||
await withClient({ onExtensionRequest: async () => { calls++; return {}; } }, async (client) => {
|
||||
|
||||
@@ -523,7 +523,21 @@ index d454fd7c5..6eba74345 100644
|
||||
}));
|
||||
result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise;
|
||||
} catch (error) {
|
||||
@@ -4864,7 +5099,7 @@ var AcpClient = class {
|
||||
@@ -4596,6 +4831,13 @@ var AcpClient = class {
|
||||
}
|
||||
}
|
||||
+ async requestExtension(method, params) {
|
||||
+ closedExtensionMethods([method]);
|
||||
+ const payload = boundedExtensionRecord(params);
|
||||
+ const connection = this.getConnection();
|
||||
+ const result = await this.runConnectionRequest(() => connection.extMethod(method, payload));
|
||||
+ return boundedExtensionRecord(result);
|
||||
+ }
|
||||
async setSessionModel(sessionId, modelId, controlOverride) {
|
||||
const control = this.resolveModelControl(sessionId, controlOverride);
|
||||
if (!control) throw new RequestedModelUnsupportedError(`Cannot set model "${modelId}": the ACP session did not advertise a model config option or legacy session/set_model support.`, "missing-capability");
|
||||
const resolvedModelId = resolveRequestedModelId({
|
||||
@@ -4864,7 +5106,7 @@ var AcpClient = class {
|
||||
}
|
||||
selectAuthMethod(methods) {
|
||||
for (const method of methods) {
|
||||
|
||||
Reference in new issue
Block a user