@platless/axom-cli 0.1.0 → 0.3.13
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/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +110 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +34 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +49 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +81 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +41 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +37 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/cli.js +9 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +32 -14
- package/dist/workstation/acp/acpAgents.js +447 -166
- package/dist/workstation/acp/acpClient.js +232 -18
- package/dist/workstation/acp/acpSession.js +502 -74
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +107 -49
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/contract.js +11 -6
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/draftInterceptor.js +9 -0
- package/dist/workstation/acp/managedPatches.js +228 -0
- package/dist/workstation/acp/patchAssets.js +63 -0
- package/dist/workstation/acp/permission.js +300 -34
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +263 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/sessionScope.js +19 -0
- package/dist/workstation/acp/sessionUpdateMapper.js +39 -21
- package/dist/workstation/acp/terminalLogin.js +197 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentSessions.js +532 -44
- package/dist/workstation/channels/agent.js +7 -1
- package/dist/workstation/channels/computer.js +152 -4
- package/dist/workstation/channels/management.js +3 -3
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +25 -3
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/fileOps.js +277 -0
- package/dist/workstation/filePaths.js +174 -0
- package/dist/workstation/fileWorker.js +81 -0
- package/dist/workstation/fileWorkerClient.js +121 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/agent.js +154 -5
- package/dist/workstation/protocol/common.js +6 -0
- package/dist/workstation/protocol/computer.js +92 -3
- package/dist/workstation/qoderAdapter/backgroundProcess.js +106 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +262 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +30 -0
- package/dist/workstation/qoderAdapter/history.js +87 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +93 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +49 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +77 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskEvent.js +26 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +269 -0
- package/dist/workstation/tools/core.js +50 -0
- package/dist/workstation/tools/files.js +525 -0
- package/dist/workstation/tools/keeperProcess.js +114 -0
- package/dist/workstation/tools/terminal.js +214 -0
- package/dist/workstation/tools/validation.js +50 -0
- package/dist/workstation/update/coordinator.js +29 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +24 -0
- package/dist/workstation/update/taskRegistry.js +44 -20
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +14 -4
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
|
@@ -3,8 +3,13 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { Readable, Writable } from 'node:stream';
|
|
4
4
|
import { client, ndJsonStream, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk';
|
|
5
5
|
import { sniffAuthUrl } from './authUrlSniffer.js';
|
|
6
|
+
import { metaTerminalAuthArgs, terminalLoginSpecOf } from './terminalLogin.js';
|
|
6
7
|
import { airCapabilityMeta, asSdkClientCapabilities, AIR_ASYNC_TASKS_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, DRAFT_SUBAGENTS_CAPABILITY, } from './draftSubagents.js';
|
|
7
8
|
import { interceptDraftUpdates } from './draftInterceptor.js';
|
|
9
|
+
import { claudeActivityRaw } from './activityReporting.js';
|
|
10
|
+
import { findOnPath, probeEnvironment } from './installProbe.js';
|
|
11
|
+
import { parseQoderProfile, sameProfileIdentity } from '../qoderAdapter/profile.js';
|
|
12
|
+
import { parseTaskObservation, taskEventMatchesHost } from '../qoderAdapter/taskEvent.js';
|
|
8
13
|
export class UnsupportedNodeVersionError extends Error {
|
|
9
14
|
}
|
|
10
15
|
/**
|
|
@@ -251,7 +256,7 @@ const STDERR_TAIL_LIMIT = 8_192;
|
|
|
251
256
|
*/
|
|
252
257
|
const STDERR_FLUSH_GRACE_MS = 250;
|
|
253
258
|
/**
|
|
254
|
-
* 把子进程的 stderr
|
|
259
|
+
* 把子进程的 stderr 接出来:普通连接原样转发,托管 MCP 请求后的输出逐行脱敏,并留一份有界的安全尾巴给报错带上,
|
|
255
260
|
* 一边逐行喂给 {@link sniffAuthUrl} —— 有的 agent(实测 antigravity)不走 `elicitation/create` 递
|
|
256
261
|
* 登录链接,而是直接打到 stderr 上(见 `authUrlSniffer.ts` 的文件头注释)。
|
|
257
262
|
*
|
|
@@ -269,9 +274,37 @@ const STDERR_FLUSH_GRACE_MS = 250;
|
|
|
269
274
|
*/
|
|
270
275
|
function captureChildStderr(stream, present) {
|
|
271
276
|
if (!stream)
|
|
272
|
-
return { text: () => '', flushed: () => Promise.resolve() };
|
|
277
|
+
return { text: () => '', flushed: () => Promise.resolve(), redactSecret: () => { } };
|
|
273
278
|
let tail = '';
|
|
274
279
|
let lineBuffer = '';
|
|
280
|
+
let protectedLine = '';
|
|
281
|
+
let droppingLongLine = false;
|
|
282
|
+
const secrets = new Set();
|
|
283
|
+
const forward = (value) => {
|
|
284
|
+
tail = (tail + value).slice(-STDERR_TAIL_LIMIT);
|
|
285
|
+
process.stderr.write(value);
|
|
286
|
+
};
|
|
287
|
+
const forwardProtected = (value) => {
|
|
288
|
+
protectedLine += value;
|
|
289
|
+
let newline;
|
|
290
|
+
while ((newline = protectedLine.indexOf('\n')) >= 0) {
|
|
291
|
+
if (droppingLongLine) {
|
|
292
|
+
droppingLongLine = false;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
let safe = protectedLine.slice(0, newline + 1);
|
|
296
|
+
for (const secret of secrets)
|
|
297
|
+
safe = safe.replaceAll(secret, '[REDACTED]');
|
|
298
|
+
forward(safe);
|
|
299
|
+
}
|
|
300
|
+
protectedLine = protectedLine.slice(newline + 1);
|
|
301
|
+
}
|
|
302
|
+
if (protectedLine.length > STDERR_TAIL_LIMIT) {
|
|
303
|
+
protectedLine = '';
|
|
304
|
+
droppingLongLine = true;
|
|
305
|
+
forward('[acp] child stderr line omitted\n');
|
|
306
|
+
}
|
|
307
|
+
};
|
|
275
308
|
// One login can print its own URL more than once — the ordinary shape of an OAuth CLI is "here's
|
|
276
309
|
// the link" immediately followed by "if your browser didn't open, here it is again", verbatim
|
|
277
310
|
// the same URL. Without this, that becomes two permanent, identical, un-retractable cards for a
|
|
@@ -315,19 +348,40 @@ function captureChildStderr(stream, present) {
|
|
|
315
348
|
};
|
|
316
349
|
stream.on('data', (chunk) => {
|
|
317
350
|
const text = chunk.toString();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
if (secrets.size > 0)
|
|
352
|
+
forwardProtected(text);
|
|
353
|
+
else {
|
|
354
|
+
tail = (tail + text).slice(-STDERR_TAIL_LIMIT);
|
|
355
|
+
process.stderr.write(chunk);
|
|
356
|
+
scanForAuthUrl(text);
|
|
357
|
+
}
|
|
321
358
|
});
|
|
322
359
|
// 读端出错(SIGKILL 掉子进程时管子会断)没有谁需要处理,但一个没有监听器的 `'error'` 会把整个
|
|
323
360
|
// 守护进程掀翻 —— 这行不是防御性代码,是 EventEmitter 的硬性要求。
|
|
324
361
|
stream.on('error', () => { });
|
|
325
362
|
// A final unterminated line (no trailing `\n` before the process exits) is still a real line —
|
|
326
363
|
// scan whatever is left in the buffer once the stream itself says there is no more coming.
|
|
327
|
-
void ended.then(() =>
|
|
364
|
+
void ended.then(() => {
|
|
365
|
+
if (protectedLine && !droppingLongLine) {
|
|
366
|
+
let safe = protectedLine;
|
|
367
|
+
for (const secret of secrets)
|
|
368
|
+
safe = safe.replaceAll(secret, '[REDACTED]');
|
|
369
|
+
forward(safe);
|
|
370
|
+
}
|
|
371
|
+
if (secrets.size === 0)
|
|
372
|
+
scanForAuthUrl('\n');
|
|
373
|
+
});
|
|
328
374
|
return {
|
|
329
375
|
text: () => tail,
|
|
330
376
|
flushed: (ms) => settledWithin(ended, ms).then(() => undefined),
|
|
377
|
+
redactSecret: (secret) => {
|
|
378
|
+
if (!secret)
|
|
379
|
+
return;
|
|
380
|
+
lineBuffer = '';
|
|
381
|
+
secrets.add(secret);
|
|
382
|
+
if (secret.startsWith('Bearer '))
|
|
383
|
+
secrets.add(secret.slice('Bearer '.length));
|
|
384
|
+
},
|
|
331
385
|
};
|
|
332
386
|
}
|
|
333
387
|
/**
|
|
@@ -471,17 +525,21 @@ export async function closeAgentTransport(child, connection, graceMs = CHILD_EXI
|
|
|
471
525
|
* Dropping rather than repairing: `id` is the value `authenticate` is called with, so inventing
|
|
472
526
|
* one would trade a missing sign-in method for a broken one.
|
|
473
527
|
*/
|
|
474
|
-
function toAuthMethodSnapshot(method) {
|
|
528
|
+
export function toAuthMethodSnapshot(method) {
|
|
475
529
|
if (typeof method.id !== 'string' || typeof method.name !== 'string')
|
|
476
530
|
return null;
|
|
531
|
+
// Terminal by shape (spec 2026-09-24 D6): a top-level `type: "terminal"`, or a `_meta
|
|
532
|
+
// ["terminal-auth"]` object from an agent that advertises its terminal login only there.
|
|
533
|
+
const terminal = terminalLoginSpecOf(method) !== undefined;
|
|
534
|
+
const args = 'args' in method && Array.isArray(method.args) ? method.args : terminal ? metaTerminalAuthArgs(method) : undefined;
|
|
477
535
|
return {
|
|
478
536
|
id: method.id,
|
|
479
537
|
name: method.name,
|
|
480
|
-
...(
|
|
538
|
+
...(terminal ? { type: 'terminal' } : {}),
|
|
481
539
|
...(method.description != null ? { description: method.description } : {}),
|
|
482
|
-
// `args` only exists on
|
|
483
|
-
//
|
|
484
|
-
...(
|
|
540
|
+
// `args` only exists on a terminal method — top-level, or under `_meta["terminal-auth"]` when
|
|
541
|
+
// that is the only place the agent put it. Never the `_meta` command: see `terminalLogins`.
|
|
542
|
+
...(args ? { args } : {}),
|
|
485
543
|
};
|
|
486
544
|
}
|
|
487
545
|
const DEFAULT_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
@@ -494,14 +552,19 @@ const DEFAULT_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
|
494
552
|
*/
|
|
495
553
|
export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
496
554
|
assertNodeVersionSupported(spec, options.nodeVersion);
|
|
555
|
+
const processEnv = { ...process.env, ...spec.env, ...env, ...agentEnv };
|
|
497
556
|
const child = spawn(spec.command, spec.args, {
|
|
498
557
|
// stderr 是 `pipe` 而不是 `inherit`,而且**依然不接进协议流** —— 它被 `captureChildStderr`
|
|
499
558
|
// 单独读走:原样转发到 daemon.log(`inherit` 以前免费提供的那件事),同时留一份尾巴给启动期的
|
|
500
559
|
// 报错。启动失败时 npm 写在这里的文字是唯一的一手证据。
|
|
501
560
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
502
|
-
env:
|
|
561
|
+
env: processEnv,
|
|
503
562
|
});
|
|
504
563
|
const stderrTail = captureChildStderr(child.stderr, options.present);
|
|
564
|
+
for (const secret of options.stderrSecrets ?? [])
|
|
565
|
+
stderrTail.redactSecret(secret);
|
|
566
|
+
if (options.onProcessExit)
|
|
567
|
+
child.once('exit', options.onProcessExit);
|
|
505
568
|
const lifecycle = watchChildLifecycle(child, spec.command, stderrTail);
|
|
506
569
|
// Argument order reads backwards from what the names suggest: argument 1 is the child's stdin
|
|
507
570
|
// (we write to it), argument 2 is the child's stdout (we read from it). Swapping them still
|
|
@@ -510,13 +573,50 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
510
573
|
// Filled in by `bindDraftUpdates` once a session exists. Held in closure variables rather than
|
|
511
574
|
// passed in, because the interceptor has to be in place before `initialize` — the agent may send
|
|
512
575
|
// its first update before anyone has had a chance to register anything.
|
|
576
|
+
let updateCapture = null;
|
|
513
577
|
let ownSessionId = null;
|
|
578
|
+
let diagnostics = {
|
|
579
|
+
generation: randomUUID(), revision: 0, available: true,
|
|
580
|
+
launch: { source: options.launchSource ?? 'direct',
|
|
581
|
+
executable: (findOnPath(spec.command, probeEnvironment(processEnv)) ?? spec.command).slice(0, 4096),
|
|
582
|
+
platform: process.platform, architecture: process.arch, transport: 'stdio' },
|
|
583
|
+
};
|
|
584
|
+
const diagnosticListeners = new Set();
|
|
585
|
+
const providerTaskListeners = new Set();
|
|
586
|
+
child.once('exit', () => {
|
|
587
|
+
diagnostics = { ...diagnostics, available: false, revision: diagnostics.revision + 1,
|
|
588
|
+
profile: diagnostics.profile ? { ...diagnostics.profile, observerHealthy: false, rootConfirmed: false } : undefined };
|
|
589
|
+
for (const listener of diagnosticListeners)
|
|
590
|
+
listener(diagnostics);
|
|
591
|
+
});
|
|
592
|
+
let adapterRevision = 0;
|
|
593
|
+
const pendingProfiles = new Map();
|
|
594
|
+
const applyProfile = (sessionId, revision, profile) => {
|
|
595
|
+
const previous = diagnostics.profile;
|
|
596
|
+
if (!previous || !diagnostics.available || sessionId !== ownSessionId || revision <= adapterRevision ||
|
|
597
|
+
!sameProfileIdentity(previous, profile) || profile.rootSessionId !== ownSessionId ||
|
|
598
|
+
(previous.runtimeGeneration !== undefined && profile.runtimeGeneration !== previous.runtimeGeneration) ||
|
|
599
|
+
(profile.rootConfirmed === true && !profile.runtimeGeneration))
|
|
600
|
+
return;
|
|
601
|
+
adapterRevision = revision;
|
|
602
|
+
diagnostics = { ...diagnostics, revision: diagnostics.revision + 1, profile };
|
|
603
|
+
for (const listener of diagnosticListeners)
|
|
604
|
+
listener(diagnostics);
|
|
605
|
+
};
|
|
514
606
|
let draftSink = null;
|
|
607
|
+
// Same "filled in later" shape as `draftSink`, for `bindActivity` — see that method's own doc
|
|
608
|
+
// comment on `AcpConnection`.
|
|
609
|
+
let activitySink = null;
|
|
515
610
|
// Sessions this connection handles by some route other than an `AcpSession` — today only
|
|
516
611
|
// adoption's replay. Kept beside `ownSessionId` rather than folded into it because they mean
|
|
517
612
|
// different things: one is "the session we drive", these are "sessions someone else on this
|
|
518
613
|
// connection is already reading".
|
|
519
614
|
const knownSessionIds = new Set();
|
|
615
|
+
// p3-fix7 N9: updates diverted before any session is bound. An agent may push its first `session/update` (Claude's
|
|
616
|
+
// `available_commands_update`) before this client has processed the `session/new` response that names the session,
|
|
617
|
+
// so it reads as a stranger's. Dropping it lost the command table of every first probe (run 4 and run 5 daemon
|
|
618
|
+
// logs); kept here, bounded, and replayed to the sink once the session binds.
|
|
619
|
+
const preBind = [];
|
|
520
620
|
const stream = ndJsonStream(Writable.toWeb(child.stdin),
|
|
521
621
|
// The child's stdout, minus the notifications the pinned SDK cannot deliver. Everything else
|
|
522
622
|
// reaches it byte-for-byte; see `draftInterceptor.ts` for the measurements behind this.
|
|
@@ -525,19 +625,66 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
525
625
|
// not been told about as our own would send its frames to the SDK, which drops them silently.
|
|
526
626
|
// Diverting is the recoverable direction — the sink can decide, and it can log.
|
|
527
627
|
isOwnSession: (sessionId) => (ownSessionId !== null && sessionId === ownSessionId) || knownSessionIds.has(sessionId),
|
|
628
|
+
captureUpdate: (notification) => updateCapture?.(notification) ?? false,
|
|
528
629
|
onDiverted: (update) => {
|
|
529
630
|
if (draftSink) {
|
|
530
631
|
draftSink(update);
|
|
531
632
|
return;
|
|
532
633
|
}
|
|
533
|
-
// No sink yet:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
634
|
+
// No sink yet: keep it until the session binds (bounded), then replay what is ours.
|
|
635
|
+
if (preBind.length >= PRE_BIND_MAX_UPDATES) {
|
|
636
|
+
const dropped = preBind.shift();
|
|
637
|
+
warnDroppedPreBind(dropped.update, 'too many updates arrived before the session was bound');
|
|
638
|
+
}
|
|
639
|
+
preBind.push({ update, at: Date.now() });
|
|
538
640
|
},
|
|
539
641
|
}));
|
|
540
642
|
const app = client({ name: 'axom' });
|
|
643
|
+
app.onNotification('_axom/qoder/profile', (params) => (params ?? {}), (ctx) => {
|
|
644
|
+
const params = ctx.params;
|
|
645
|
+
const profile = parseQoderProfile(params.profile);
|
|
646
|
+
if (!diagnostics.profile || !profile || typeof params.sessionId !== 'string' ||
|
|
647
|
+
profile.rootSessionId !== params.sessionId || !Number.isSafeInteger(params.revision) ||
|
|
648
|
+
params.revision <= 0 || !sameProfileIdentity(diagnostics.profile, profile))
|
|
649
|
+
return;
|
|
650
|
+
if (ownSessionId) {
|
|
651
|
+
applyProfile(params.sessionId, params.revision, profile);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// ACP may notify before its new/load reply. Retain only bounded typed candidates;
|
|
655
|
+
// no candidate becomes evidence until the response binds this connection's exact root.
|
|
656
|
+
const previous = pendingProfiles.get(params.sessionId);
|
|
657
|
+
if (previous && (params.revision <= previous.revision ||
|
|
658
|
+
profile.runtimeGeneration !== previous.profile.runtimeGeneration))
|
|
659
|
+
return;
|
|
660
|
+
if (!previous && pendingProfiles.size >= 8)
|
|
661
|
+
return;
|
|
662
|
+
pendingProfiles.set(params.sessionId, { revision: params.revision, profile });
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
app.onNotification('_qoder/task_event', (params) => params, (ctx) => {
|
|
666
|
+
const event = parseTaskObservation(ctx.params);
|
|
667
|
+
if (!event || !taskEventMatchesHost(event, {
|
|
668
|
+
selected: options.providerTaskAdapterSelected === true, available: diagnostics.available,
|
|
669
|
+
confirmed: diagnostics.profile?.rootConfirmed === true, rootSessionId: ownSessionId,
|
|
670
|
+
runtimeGeneration: diagnostics.profile?.runtimeGeneration,
|
|
671
|
+
}))
|
|
672
|
+
return;
|
|
673
|
+
for (const listener of providerTaskListeners)
|
|
674
|
+
listener(event);
|
|
675
|
+
});
|
|
676
|
+
// claude-agent-acp's raw-SDK-message channel (design §5.1/§2.1): registered unconditionally on
|
|
677
|
+
// every connection, before `options.configure?.(app)` and before `initialize` — `_claude/sdkMessage`
|
|
678
|
+
// is not a built-in ACP method, so `ClientApp.onNotification` needs the custom-method overload (a
|
|
679
|
+
// parser plus a handler) rather than the typed-literal one `registerPermissionHandlers` uses. The
|
|
680
|
+
// method name is vendor-specific, so this handler simply never fires on a connection to any other
|
|
681
|
+
// agent — there is no need to gate the registration on `agentInfo`, which in any case is not yet
|
|
682
|
+
// known this early (`initialize` has not happened).
|
|
683
|
+
app.onNotification('_claude/sdkMessage', (params) => (params ?? {}), (ctx) => {
|
|
684
|
+
const raw = claudeActivityRaw(ctx.params);
|
|
685
|
+
if (raw)
|
|
686
|
+
activitySink?.(raw);
|
|
687
|
+
});
|
|
541
688
|
options.configure?.(app);
|
|
542
689
|
const connection = app.connect(stream);
|
|
543
690
|
const timeoutMs = options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
@@ -595,7 +742,14 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
595
742
|
// `clientCapabilities?.elicitation?.url != null`. Without declaring it, Codex never offers
|
|
596
743
|
// that method and `elicitation/create` with `mode: 'url'` never arrives to answer (see
|
|
597
744
|
// `permission.ts`'s `handleCreateElicitation`).
|
|
598
|
-
|
|
745
|
+
//
|
|
746
|
+
// `form` (`ElicitationFormCapabilities`, `types.gen.d.ts:4455`) is what keeps Claude's
|
|
747
|
+
// AskUserQuestion alive: without it the adapter puts the tool in `disallowedTools`
|
|
748
|
+
// (`claude-agent-acp/dist/acp-agent.js:5959`), so the model never has it at all — the
|
|
749
|
+
// symptom is prose questions, not an unrendered card. This is the trigger for the whole
|
|
750
|
+
// elicitation-standardization plan: everything through Task 17 built the machinery to
|
|
751
|
+
// answer `elicitation/create` once it arrives; this line is what makes it arrive at all.
|
|
752
|
+
elicitation: { url: {}, form: {} },
|
|
599
753
|
_meta: {
|
|
600
754
|
'subagent-transcript': true,
|
|
601
755
|
...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
|
|
@@ -630,12 +784,14 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
630
784
|
throw error;
|
|
631
785
|
}
|
|
632
786
|
const sessionCapabilities = initResponse.agentCapabilities?.sessionCapabilities;
|
|
787
|
+
diagnostics = { ...diagnostics, profile: parseQoderProfile(initResponse._meta?.qoderProfile) };
|
|
633
788
|
const capabilities = {
|
|
634
789
|
// Capability markers are empty objects (`{ _meta? }`); supplying `{}` means "supported", and
|
|
635
790
|
// omitting the field (or explicit `null`) means "not supported". A truthiness check reads `{}`
|
|
636
791
|
// correctly by accident but reads `null`/`undefined` correctly too, which hides the real bug:
|
|
637
792
|
// this must stay an explicit `!= null`, never `Boolean(...)` or a bare truthy test.
|
|
638
793
|
list: sessionCapabilities?.list != null,
|
|
794
|
+
delete: sessionCapabilities?.delete != null,
|
|
639
795
|
resume: sessionCapabilities?.resume != null,
|
|
640
796
|
loadSession: initResponse.agentCapabilities?.loadSession === true,
|
|
641
797
|
logout: initResponse.agentCapabilities?.auth?.logout != null,
|
|
@@ -649,6 +805,7 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
649
805
|
* is a field nobody can act on.
|
|
650
806
|
*/
|
|
651
807
|
promptImage: initResponse.agentCapabilities?.promptCapabilities?.image === true,
|
|
808
|
+
mcpHttp: initResponse.agentCapabilities?.mcpCapabilities?.http === true,
|
|
652
809
|
};
|
|
653
810
|
// Honour the advertised capability, not what the agent happens to route: some adapters (e.g.
|
|
654
811
|
// codex-acp routing `session/fork`) implement more than they declare in `agentCapabilities`.
|
|
@@ -658,6 +815,12 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
658
815
|
const authMethods = advertisedAuthMethods
|
|
659
816
|
.map(toAuthMethodSnapshot)
|
|
660
817
|
.filter((method) => method !== null);
|
|
818
|
+
const terminalLogins = new Map();
|
|
819
|
+
for (const method of advertisedAuthMethods) {
|
|
820
|
+
const spec = terminalLoginSpecOf(method);
|
|
821
|
+
if (spec && typeof method.id === 'string')
|
|
822
|
+
terminalLogins.set(method.id, spec);
|
|
823
|
+
}
|
|
661
824
|
if (authMethods.length !== advertisedAuthMethods.length) {
|
|
662
825
|
// Named and counted rather than dropped in silence: the user loses a sign-in method they can
|
|
663
826
|
// see advertised in the agent's own docs, and the only honest answer to "why" is which agent
|
|
@@ -669,16 +832,60 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
669
832
|
'id/name; ignoring them');
|
|
670
833
|
}
|
|
671
834
|
return {
|
|
835
|
+
get runtimeDiagnostics() { return diagnostics; },
|
|
836
|
+
providerTaskAdapterSelected: options.providerTaskAdapterSelected === true,
|
|
837
|
+
publishCallbacks(snapshot) {
|
|
838
|
+
if (snapshot.rootSessionId !== ownSessionId)
|
|
839
|
+
return;
|
|
840
|
+
diagnostics = { ...diagnostics, revision: diagnostics.revision + 1, callbacks: snapshot };
|
|
841
|
+
for (const listener of diagnosticListeners)
|
|
842
|
+
listener(diagnostics);
|
|
843
|
+
},
|
|
844
|
+
onRuntimeDiagnostics(cb) { diagnosticListeners.add(cb); },
|
|
845
|
+
onProviderTaskEvent(cb) { providerTaskListeners.add(cb); },
|
|
672
846
|
capabilities,
|
|
673
847
|
authMethods,
|
|
848
|
+
terminalLogins,
|
|
674
849
|
supportsSteering: steeringAdvertised(initResponse._meta),
|
|
675
850
|
steeringTimings: steeringTimings(initResponse._meta),
|
|
676
851
|
agentInfo: initResponse.agentInfo ?? undefined,
|
|
677
852
|
connection,
|
|
853
|
+
redactStderrSecret: (secret) => stderrTail.redactSecret(secret),
|
|
678
854
|
agentProcessAlive: () => child.exitCode === null && child.signalCode === null,
|
|
855
|
+
captureSessionUpdates(sink) {
|
|
856
|
+
if (updateCapture)
|
|
857
|
+
throw new Error('A session replay is already active on this connection');
|
|
858
|
+
updateCapture = sink;
|
|
859
|
+
return () => { if (updateCapture === sink)
|
|
860
|
+
updateCapture = null; };
|
|
861
|
+
},
|
|
679
862
|
bindDraftUpdates(sessionId, sink) {
|
|
680
863
|
ownSessionId = sessionId;
|
|
681
864
|
draftSink = sink;
|
|
865
|
+
const current = diagnostics.profile;
|
|
866
|
+
if (current?.rootSessionId && current.rootSessionId !== sessionId) {
|
|
867
|
+
diagnostics = { ...diagnostics, profile: { ...current, rootSessionId: undefined,
|
|
868
|
+
runtimeGeneration: undefined, rootConfirmed: false, initialized: false, observerHealthy: false } };
|
|
869
|
+
}
|
|
870
|
+
const pending = pendingProfiles.get(sessionId);
|
|
871
|
+
pendingProfiles.clear();
|
|
872
|
+
if (pending)
|
|
873
|
+
applyProfile(sessionId, pending.revision, pending.profile);
|
|
874
|
+
// p3-fix7 N9: replay what arrived before this moment. Our own session's updates go to the sink as `pre-bind`
|
|
875
|
+
// (they are ours, not a child's); anything else, or anything older than the window, is dropped with the
|
|
876
|
+
// warning it always had.
|
|
877
|
+
const now = Date.now();
|
|
878
|
+
for (const { update, at } of preBind.splice(0)) {
|
|
879
|
+
if (update.sessionId === sessionId && now - at <= PRE_BIND_MAX_AGE_MS) {
|
|
880
|
+
sink(update.reason === 'foreign-session' ? { ...update, reason: 'pre-bind' } : update);
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
warnDroppedPreBind(update, 'no session is bound to this connection yet');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
bindActivity(sink) {
|
|
888
|
+
activitySink = sink;
|
|
682
889
|
},
|
|
683
890
|
markSessionKnown(sessionId) {
|
|
684
891
|
knownSessionIds.add(sessionId);
|
|
@@ -689,3 +896,10 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
689
896
|
close: () => closeAgentTransport(child, connection),
|
|
690
897
|
};
|
|
691
898
|
}
|
|
899
|
+
/** p3-fix7 N9: how many updates, and for how long, a connection keeps before its session is bound. */
|
|
900
|
+
const PRE_BIND_MAX_UPDATES = 32;
|
|
901
|
+
const PRE_BIND_MAX_AGE_MS = 5_000;
|
|
902
|
+
/** Says so once per update rather than dropping in silence; it should stay rare, and if it stops being rare it is worth seeing. */
|
|
903
|
+
function warnDroppedPreBind(update, why) {
|
|
904
|
+
console.warn(`[acp] dropped a ${update.reason} session/update for '${update.sessionId}': ${why}`);
|
|
905
|
+
}
|