@ours.network/fleet 1.2.0-nightly.53 → 1.2.0-nightly.55
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 +22 -12
- package/dist/agent-ours/bridge.js +10 -3
- package/dist/agent-ours/file-rpc.d.ts +8 -0
- package/dist/agent-ours/file-rpc.js +11 -1
- package/dist/agent-ours/harness.js +47 -3
- package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
- package/dist/agent-ours/mcp-endpoint.js +4 -0
- package/dist/agent-ours/service.d.ts +6 -1
- package/dist/agent-ours/service.js +73 -8
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/supervisor-ours-tools.js +3 -1
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +11 -0
- package/dist/config.js +13 -2
- package/dist/file-delivery/reader.d.ts +17 -0
- package/dist/file-delivery/reader.js +94 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-app-server-proxy.js +6 -2
- package/dist/harness/codex-file-inventory.d.ts +15 -0
- package/dist/harness/codex-file-inventory.js +113 -0
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +2 -1
- package/dist/isolation/policy.js +4 -1
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +161 -22
- package/dist/session/acp.d.ts +5 -0
- package/dist/session/acp.js +68 -8
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +3 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +7 -3
|
@@ -664,6 +664,8 @@ function findCursorForSeat(id, stored, seat) {
|
|
|
664
664
|
if (cursor && seat.identity_cid
|
|
665
665
|
&& cursor.identity_cid.toLowerCase() !== seat.identity_cid.toLowerCase())
|
|
666
666
|
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' identity CID mismatch between cursor and room seat`);
|
|
667
|
+
if (cursor?.action_id && seat.launch?.action_id && cursor.action_id !== seat.launch.action_id)
|
|
668
|
+
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' creation ownership proof changed`);
|
|
667
669
|
return cursor;
|
|
668
670
|
}
|
|
669
671
|
/**
|
|
@@ -684,11 +686,20 @@ export function upsertTaskDeletionMembersFromSeats(id, seats) {
|
|
|
684
686
|
for (const seat of seats) {
|
|
685
687
|
if (!seat.identity_cid)
|
|
686
688
|
continue;
|
|
687
|
-
|
|
689
|
+
const cursor = findCursorForSeat(id, stored, seat);
|
|
690
|
+
if (!cursor) {
|
|
688
691
|
stored.deletion.members.push({
|
|
689
692
|
name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now,
|
|
693
|
+
...(seat.launch?.action_id ? { action_id: seat.launch.action_id } : {}),
|
|
694
|
+
...(seat.launch?.task_supervised ? { task_supervised: true } : {}),
|
|
690
695
|
});
|
|
691
696
|
}
|
|
697
|
+
else {
|
|
698
|
+
if (seat.launch?.action_id)
|
|
699
|
+
cursor.action_id ??= seat.launch.action_id;
|
|
700
|
+
if (seat.launch?.task_supervised)
|
|
701
|
+
cursor.task_supervised = true;
|
|
702
|
+
}
|
|
692
703
|
}
|
|
693
704
|
writeTask(stored);
|
|
694
705
|
return presentTaskLenient(stored);
|
|
@@ -747,6 +758,10 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
|
|
|
747
758
|
cursor = { name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now };
|
|
748
759
|
stored.deletion.members.push(cursor);
|
|
749
760
|
}
|
|
761
|
+
if (seat.launch?.action_id)
|
|
762
|
+
cursor.action_id ??= seat.launch.action_id;
|
|
763
|
+
if (seat.launch?.task_supervised)
|
|
764
|
+
cursor.task_supervised = true;
|
|
750
765
|
if (DELETION_MEMBER_PHASE_ORDER[evidence.phase] < DELETION_MEMBER_PHASE_ORDER[cursor.phase])
|
|
751
766
|
continue;
|
|
752
767
|
if (cursor.launch_id !== undefined && cursor.launch_id !== evidence.launch_id)
|
|
@@ -62,6 +62,8 @@ export type TaskDeletionMemberPhase = 'pending' | 'stop_requested' | 'liveness_a
|
|
|
62
62
|
export interface TaskDeletionMemberCursor {
|
|
63
63
|
name: string;
|
|
64
64
|
identity_cid: string;
|
|
65
|
+
action_id?: string;
|
|
66
|
+
task_supervised?: boolean;
|
|
65
67
|
phase: TaskDeletionMemberPhase;
|
|
66
68
|
launch_id?: string;
|
|
67
69
|
archive_path?: string;
|
|
@@ -170,6 +172,7 @@ export interface RoomRoleBriefingDefinition {
|
|
|
170
172
|
last_error?: string;
|
|
171
173
|
}
|
|
172
174
|
export interface RoomMemberLaunchState {
|
|
175
|
+
task_supervised?: boolean;
|
|
173
176
|
state: 'pending' | 'intent' | 'launched' | 'stopped' | 'failed';
|
|
174
177
|
attempt: number;
|
|
175
178
|
action_id?: string;
|
|
@@ -11,6 +11,8 @@ export declare class WorkspaceError extends Error {
|
|
|
11
11
|
export declare function assertSafeAncestors(path: string): void;
|
|
12
12
|
export declare function planWorkspace(owner: OwnedWorkspace['owner'], id: string): OwnedWorkspace;
|
|
13
13
|
export declare function validateWorkspace(w: OwnedWorkspace, owner?: "task" | "room", id?: string): void;
|
|
14
|
+
/** Read-only recovery proof; never allocate a replacement for a missing workspace. */
|
|
15
|
+
export declare function assertWorkspacePresent(w: OwnedWorkspace, owner: OwnedWorkspace['owner'], id: string): void;
|
|
14
16
|
/** Caller must first prove the exact workspace marker or retired archive
|
|
15
17
|
* provenance, and hold its lifecycle lock with all managed writers retired.
|
|
16
18
|
* Directories need owner rwx for recursive removal; ordinary files do not.
|
|
@@ -54,6 +54,11 @@ function marked(path, w) {
|
|
|
54
54
|
if (JSON.stringify(actual) !== JSON.stringify(w))
|
|
55
55
|
fail('ownership marker mismatch');
|
|
56
56
|
}
|
|
57
|
+
/** Read-only recovery proof; never allocate a replacement for a missing workspace. */
|
|
58
|
+
export function assertWorkspacePresent(w, owner, id) {
|
|
59
|
+
validateWorkspace(w, owner, id);
|
|
60
|
+
marked(w.path, w);
|
|
61
|
+
}
|
|
57
62
|
function staging(w, phase) {
|
|
58
63
|
return join(dirname(w.path), `.${w.id}.${w.token}.${phase}`);
|
|
59
64
|
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface RunnerDeps {
|
|
|
39
39
|
reportOwnerStartupFailure(stateDir: string): Promise<'delivered' | 'duplicate'>;
|
|
40
40
|
/** Lets a test (or a shutdown path) end the supervised restart loop. */
|
|
41
41
|
shouldStop?(): boolean;
|
|
42
|
+
/** Parent shutdown suspends a managed transient member; explicit stop still retires. */
|
|
43
|
+
suspendOnStop?: boolean;
|
|
42
44
|
}
|
|
43
45
|
export declare const SUPERVISOR_RECYCLE_REQUIRED = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
|
|
44
46
|
export declare class SupervisorRecycleRequiredError extends Error {
|
|
@@ -201,9 +203,13 @@ export declare function runOnce(name: string, opts?: {
|
|
|
201
203
|
*/
|
|
202
204
|
export declare function runSupervised(name: string, opts?: {
|
|
203
205
|
configPath?: string;
|
|
206
|
+
temp?: boolean;
|
|
204
207
|
}, partialDeps?: Partial<RunnerDeps>, attempt?: (n: string, o: {
|
|
205
208
|
configPath?: string;
|
|
206
209
|
allowResumeRotation?: boolean;
|
|
210
|
+
temp?: boolean;
|
|
207
211
|
}, d: Partial<RunnerDeps>) => Promise<AttemptResult>): Promise<RestartLedger>;
|
|
208
212
|
/** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
|
|
213
|
+
/** The parent uses the same adapter-relative sustained-uptime reset boundary. */
|
|
214
|
+
export declare function stableSupervisorWindow(name: string, temp: boolean, configPath?: string): number;
|
|
209
215
|
export declare function runTemp(name: string, deps?: Partial<RunnerDeps>, attempt?: typeof runOnce): Promise<void>;
|
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { readProvenance } from './creation.js';
|
|
2
|
+
import { firstManagedSessionMayStart, managedRecoveryPath, readManagedRecovery, writeManagedRecovery } from './managed-recovery.js';
|
|
1
3
|
import { StartupTelemetry } from './startup-telemetry.js';
|
|
2
4
|
import { managedStartupPrompt, managedTaskPrompt } from './startup-prompt.js';
|
|
3
5
|
import { createLayoutControl } from './rooms-tasks/layout-control.js';
|
|
4
6
|
import { TemporaryChatIdle } from './temp-idle.js';
|
|
7
|
+
import { taskSupervisorMayRun, taskMemberRetirementProof } from './task-supervision.js';
|
|
5
8
|
import { prepareManagedAgent, releaseManagedAgent } from './agent-ours/service.js';
|
|
6
9
|
import { prepareManagedHarness } from './agent-ours/harness.js';
|
|
7
10
|
import { prepareManagedCliLaunch } from './managed-cli.js';
|
|
@@ -33,7 +36,7 @@ import { ScheduledLoopManager, } from './loops/manager.js';
|
|
|
33
36
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
|
|
34
37
|
import { effectivePermissionMode } from './permissions.js';
|
|
35
38
|
import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
|
|
36
|
-
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
|
|
39
|
+
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, readTempSupervisor, } from './temp-lifecycle.js';
|
|
37
40
|
import { FleetCommandAuditStore } from './fleet-command-audit.js';
|
|
38
41
|
export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
|
|
39
42
|
export class SupervisorRecycleRequiredError extends Error {
|
|
@@ -511,14 +514,43 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
511
514
|
deps.log(`[${name}] worklog rotation deferred: concurrent modification detected`);
|
|
512
515
|
else if (rotation.rotated)
|
|
513
516
|
deps.log(`[${name}] worklog rotated: ${rotation.beforeBytes} -> ${rotation.afterBytes} bytes`);
|
|
517
|
+
const durableTask = Boolean(readTempSupervisor(dir)?.taskOwner);
|
|
518
|
+
const retainedManaged = durableTask || Boolean(deps.suspendOnStop && adapter.supportsResume);
|
|
514
519
|
const sidFile = join(dir, '.session-id');
|
|
520
|
+
const backendSidFile = join(dir, role.session === 'codex-app-server' ? '.session-id' : '.acp-session-id');
|
|
521
|
+
let recovery = retainedManaged ? readManagedRecovery(dir) : undefined;
|
|
522
|
+
// Native Codex shares .session-id with the runner's pre-launch UUID, so file
|
|
523
|
+
// existence alone is not evidence that its first backend thread was created.
|
|
524
|
+
const firstSessionRetry = retainedManaged && existsSync(join(dir, '.booted'))
|
|
525
|
+
&& (!existsSync(backendSidFile) || (role.session === 'codex-app-server' && recovery?.session === 'pending'))
|
|
526
|
+
&& firstManagedSessionMayStart(dir);
|
|
527
|
+
if (retainedManaged && existsSync(join(dir, '.booted'))
|
|
528
|
+
&& (!existsSync(sidFile) || !existsSync(backendSidFile)) && !firstSessionRetry)
|
|
529
|
+
throw Error('TASK_CONTEXT_RESUME_UNAVAILABLE');
|
|
515
530
|
if (!existsSync(sidFile))
|
|
516
531
|
writeFileSync(sidFile, randomUUID() + '\n');
|
|
517
532
|
const sessionId = readFileSync(sidFile, 'utf8').trim();
|
|
518
533
|
const bootedFile = join(dir, '.booted');
|
|
519
534
|
const exitFile = join(dir, '.exit-status');
|
|
520
535
|
const booted = existsSync(bootedFile);
|
|
521
|
-
|
|
536
|
+
if (durableTask && !adapter.supportsResume)
|
|
537
|
+
throw Error('TASK_HARNESS_RESUME_UNSUPPORTED');
|
|
538
|
+
const mode = booted && adapter.supportsResume && !firstSessionRetry ? 'resume' : 'fresh';
|
|
539
|
+
const requireResume = retainedManaged && mode === 'resume';
|
|
540
|
+
if (retainedManaged && !recovery) {
|
|
541
|
+
// Older admitted sessions have no cursor. Continue once, without replaying
|
|
542
|
+
// their assignment; fresh sessions record intent before the first attempt.
|
|
543
|
+
recovery = { version: 1, session: booted ? 'established' : 'pending', readiness: booted,
|
|
544
|
+
initial: booted ? 'dispatching' : 'pending', active: booted };
|
|
545
|
+
writeManagedRecovery(dir, recovery);
|
|
546
|
+
}
|
|
547
|
+
const recoveryBefore = recovery ? { ...recovery } : undefined;
|
|
548
|
+
const saveRecovery = (update) => {
|
|
549
|
+
if (!recovery)
|
|
550
|
+
return;
|
|
551
|
+
recovery = { ...recovery, ...update };
|
|
552
|
+
writeManagedRecovery(dir, recovery);
|
|
553
|
+
};
|
|
522
554
|
// Stamp EVERY attempt, not just the first. `.booted` used to be written only
|
|
523
555
|
// on the fresh path, so after a restart — including one the supervisor never
|
|
524
556
|
// saw, like an OOM-kill — its mtime still read the original boot and any
|
|
@@ -672,10 +704,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
672
704
|
managedOurs: managedHarness.ours,
|
|
673
705
|
role, prep,
|
|
674
706
|
launch: { ...launch, argv: wrappedArgv, env: managedHarness.env },
|
|
675
|
-
cwd: runCwd, stateDir: dir, mode, permissions: perms,
|
|
707
|
+
cwd: runCwd, stateDir: dir, mode, requireResume, permissions: perms,
|
|
676
708
|
permissionMode: effectivePermissionMode(role), log: deps.log,
|
|
677
709
|
}));
|
|
710
|
+
managedService.setFileDelivery?.(agentSession.sendFileToChat?.bind(agentSession));
|
|
678
711
|
startupTelemetry.mark('harness_ready');
|
|
712
|
+
saveRecovery({ session: 'established' });
|
|
679
713
|
pid = agentSession.pid;
|
|
680
714
|
arbiter = new RoleTurnArbiter(agentSession);
|
|
681
715
|
sessionHandle = arbiter;
|
|
@@ -696,7 +730,25 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
696
730
|
startMonitorAfterStartup();
|
|
697
731
|
}
|
|
698
732
|
});
|
|
699
|
-
|
|
733
|
+
const activePrompts = new Set();
|
|
734
|
+
const unsubscribeContinuity = agentSession.subscribeConversation?.(event => {
|
|
735
|
+
if (!recovery?.readiness)
|
|
736
|
+
return;
|
|
737
|
+
if (event.kind === 'prompt.started') {
|
|
738
|
+
if (event.promptId)
|
|
739
|
+
activePrompts.add(event.promptId);
|
|
740
|
+
saveRecovery({ active: true });
|
|
741
|
+
}
|
|
742
|
+
else if (event.kind === 'turn.completed' && 'outcome' in event.payload) {
|
|
743
|
+
if (event.promptId)
|
|
744
|
+
activePrompts.delete(event.promptId);
|
|
745
|
+
// A shutdown/failed turn needs continuation; a successful terminal turn
|
|
746
|
+
// proves it may stay idle. Never store prompt bodies in this cursor.
|
|
747
|
+
if (event.payload.outcome === 'completed')
|
|
748
|
+
saveRecovery({ active: activePrompts.size > 0 });
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
unsubscribeRecovery = () => { unsubscribeFailure(); unsubscribeStartup?.(); unsubscribeTelemetry?.(); unsubscribeContinuity?.(); };
|
|
700
752
|
if (role.owner_channel) {
|
|
701
753
|
try {
|
|
702
754
|
ownerBinder = await deps.acquireOwnerBinder(dir, name, role.owner_channel.identity);
|
|
@@ -817,8 +869,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
817
869
|
// Wait for the first turn's TERMINAL result. An agent that accepts the
|
|
818
870
|
// startup prompt and then refuses it has not started; logging the role as
|
|
819
871
|
// up would hide a role that never completed its readiness turn.
|
|
820
|
-
|
|
821
|
-
|
|
872
|
+
const retainedResume = retainedManaged && mode === 'resume' && recoveryBefore?.readiness === true;
|
|
873
|
+
if (!retainedResume)
|
|
874
|
+
startupTelemetry.mark('startup_submitted');
|
|
875
|
+
// The backend already verified the saved conversation. Resuming a managed
|
|
876
|
+
// member admits new wakes/console input without replaying startup or work.
|
|
877
|
+
const starting = retainedResume
|
|
878
|
+
? Promise.resolve({ accepted: false, succeeded: true, outcome: 'completed', detail: 'retained backend resumed; no readiness prompt submitted' })
|
|
879
|
+
: arbiter.submitPrompt(firstPrompt, { origin: { kind: 'startup' } });
|
|
822
880
|
// Keep the early cursor baseline; do not deliver wakes into the protected
|
|
823
881
|
// first turn. Steering can pre-empt startup just like cancellation.
|
|
824
882
|
// Owner traffic and agent replies must be observed during a long first
|
|
@@ -883,20 +941,36 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
883
941
|
+ 'keeping temporary supervisor alive');
|
|
884
942
|
startupRecoveryAllowed = interruptedForWake;
|
|
885
943
|
sessionStartupComplete = started.succeeded || (interruptedForWake && successfulTurnObserved);
|
|
886
|
-
if (started.succeeded)
|
|
944
|
+
if (started.succeeded && !retainedResume)
|
|
887
945
|
startupTelemetry.mark('readiness_turn_completed');
|
|
946
|
+
if (started.succeeded)
|
|
947
|
+
saveRecovery({ readiness: true });
|
|
888
948
|
// Readiness is now complete. Configured work (or retained continuity) is
|
|
889
949
|
// admitted as a distinct turn; its terminal result must not block console
|
|
890
950
|
// readiness, mail delivery, or supervisor liveness. Empty fresh agents wait
|
|
891
951
|
// for authenticated console input or the monitor's unread/arrival wake.
|
|
892
|
-
|
|
893
|
-
|
|
952
|
+
const configuredWork = Boolean(role.mission?.trim() || role.roomMemberStartup?.task?.trim() || role.briefing_file?.trim());
|
|
953
|
+
const initialPending = retainedManaged && recoveryBefore?.initial === 'pending';
|
|
954
|
+
const interruptedWork = retainedManaged && (recoveryBefore?.active || recoveryBefore?.initial === 'dispatching');
|
|
955
|
+
const workPrompt = retainedManaged
|
|
956
|
+
? initialPending && configuredWork ? managedTaskPrompt(dir, 'fresh', false)
|
|
957
|
+
: interruptedWork ? 'Fleet supervisor restarted. Continue pending work in the restored conversation; do not redo completed steps. '
|
|
958
|
+
+ (recoveryBefore?.initial === 'dispatching' ? `If you have not yet received your assignment, read ${join(dir, 'briefing.md')} once before acting. ` : '')
|
|
959
|
+
+ 'Read the continuation log only as needed to recover your next action. If no work remains, end this turn and wait for a concrete task or supervisor wake.' : undefined
|
|
960
|
+
: configuredWork || booted ? managedTaskPrompt(dir, mode, booted) : undefined;
|
|
961
|
+
if (started.succeeded && !deps.shouldStop?.() && workPrompt) {
|
|
962
|
+
saveRecovery({ initial: 'dispatching', active: true });
|
|
963
|
+
const queued = await arbiter.queuePrompt(workPrompt, {
|
|
894
964
|
origin: { kind: 'startup' },
|
|
895
965
|
});
|
|
896
966
|
void queued.completion.then(result => {
|
|
967
|
+
if (result.succeeded)
|
|
968
|
+
saveRecovery({ initial: 'completed', active: activePrompts.size > 0 });
|
|
897
969
|
deps.log(`[${name}] configured task turn ${result.outcome}`);
|
|
898
970
|
}, () => deps.log(`[${name}] configured task turn failed`));
|
|
899
971
|
}
|
|
972
|
+
else if (started.succeeded && initialPending && !configuredWork)
|
|
973
|
+
saveRecovery({ initial: 'completed' });
|
|
900
974
|
reloadLoopConfig = async () => {
|
|
901
975
|
const nextRole = findRole(loadConfig(configPath), name);
|
|
902
976
|
const definitions = nextRole.loops ?? [];
|
|
@@ -985,7 +1059,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
985
1059
|
});
|
|
986
1060
|
let chatIdle;
|
|
987
1061
|
try {
|
|
988
|
-
if (temp && !role.roomMemberStartup)
|
|
1062
|
+
if (temp && !role.roomMemberStartup && !readTempSupervisor(dir)?.taskOwner)
|
|
989
1063
|
chatIdle = new TemporaryChatIdle(dir, deps.now());
|
|
990
1064
|
}
|
|
991
1065
|
catch {
|
|
@@ -1121,11 +1195,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1121
1195
|
const rotate = (why) => {
|
|
1122
1196
|
writeFileSync(sidFile, randomUUID() + '\n');
|
|
1123
1197
|
rmSync(bootedFile, { force: true });
|
|
1198
|
+
rmSync(managedRecoveryPath(dir), { force: true });
|
|
1124
1199
|
rotated = true;
|
|
1125
1200
|
deps.log(`[${name}] ${why} -> rotated session-id; next start is FRESH`);
|
|
1126
1201
|
};
|
|
1127
|
-
if (!temp && deps.shouldStop?.())
|
|
1128
|
-
deps.log(`[${name}] supervisor
|
|
1202
|
+
if (readTempSupervisor(dir)?.taskOwner || ((!temp || deps.suspendOnStop) && deps.shouldStop?.()))
|
|
1203
|
+
deps.log(`[${name}] supervisor/context retained -> next start RESUMES context`);
|
|
1129
1204
|
else if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
|
|
1130
1205
|
|| exitRecord.detail.includes(CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED))
|
|
1131
1206
|
// This is a deliberate adapter reclamation, not evidence that resume state
|
|
@@ -1175,11 +1250,13 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1175
1250
|
*/
|
|
1176
1251
|
export async function runSupervised(name, opts = {}, partialDeps = {}, attempt = runOnce) {
|
|
1177
1252
|
const deps = { ...defaultDeps(), ...partialDeps };
|
|
1178
|
-
const
|
|
1253
|
+
const taskMember = opts.temp === true;
|
|
1254
|
+
const dir = agentDir(name, taskMember);
|
|
1179
1255
|
mkdirSync(dir, { recursive: true });
|
|
1180
1256
|
let stopping = false;
|
|
1181
1257
|
const requestStop = () => { stopping = true; };
|
|
1182
|
-
const shouldStop = () => stopping || (partialDeps.shouldStop?.() ?? false)
|
|
1258
|
+
const shouldStop = () => stopping || (partialDeps.shouldStop?.() ?? false)
|
|
1259
|
+
|| (taskMember && requestedTempStopReason(dir) === 'operator-stop');
|
|
1183
1260
|
deps.shouldStop = shouldStop;
|
|
1184
1261
|
const stamp = () => new Date(deps.now()).toISOString();
|
|
1185
1262
|
// Record how the PREVIOUS supervisor process ended before doing anything
|
|
@@ -1207,10 +1284,12 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1207
1284
|
process.on('SIGINT', requestStop);
|
|
1208
1285
|
try {
|
|
1209
1286
|
while (!shouldStop()) {
|
|
1287
|
+
if (taskMember && !taskSupervisorMayRun(name))
|
|
1288
|
+
break;
|
|
1210
1289
|
let ledger = readRestartLedger(dir);
|
|
1211
1290
|
try {
|
|
1212
1291
|
const configPath = resolveConfigPath(dir, opts.configPath);
|
|
1213
|
-
const role = findRole(loadConfig(configPath), name);
|
|
1292
|
+
const role = taskMember ? loadTempRole(name) : findRole(loadConfig(configPath), name);
|
|
1214
1293
|
reconcileModelRecovery(dir, role, stamp());
|
|
1215
1294
|
if (modelRecoveryHeld(dir)) {
|
|
1216
1295
|
await deps.sleep(HELD_DOWN_POLL_MS);
|
|
@@ -1226,18 +1305,26 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1226
1305
|
await deps.sleep(HELD_DOWN_POLL_MS);
|
|
1227
1306
|
continue;
|
|
1228
1307
|
}
|
|
1308
|
+
if (attempt === runOnce && taskMember) {
|
|
1309
|
+
// Boot-time daemon unavailability must not consume the harness crash budget.
|
|
1310
|
+
const { waitForRoleDaemon } = await import('./startup-readiness.js');
|
|
1311
|
+
await waitForRoleDaemon(name, undefined, {}, true);
|
|
1312
|
+
}
|
|
1313
|
+
if (taskMember && (shouldStop() || !taskSupervisorMayRun(name)))
|
|
1314
|
+
break;
|
|
1229
1315
|
let result;
|
|
1230
1316
|
try {
|
|
1231
1317
|
// Service-manager boot and automatic retry bypass the operator-facing
|
|
1232
1318
|
// up/restart commands. Reconcile again immediately before every real
|
|
1233
1319
|
// permanent harness attempt; the operation is idempotent and releases its
|
|
1234
1320
|
// provisioning lease before the agent or owner channel binds.
|
|
1235
|
-
if (attempt === runOnce) {
|
|
1321
|
+
if (attempt === runOnce && !taskMember) {
|
|
1236
1322
|
const configPath = resolveConfigPath(dir, opts.configPath);
|
|
1237
1323
|
const role = findRole(loadConfig(configPath), name);
|
|
1238
1324
|
await reconcilePermanentRoleIdentities(role, undefined, deps.log);
|
|
1239
1325
|
}
|
|
1240
|
-
result = await attempt(name, { configPath: opts.configPath,
|
|
1326
|
+
result = await attempt(name, { configPath: opts.configPath, ...(taskMember ? { temp: true } : {}),
|
|
1327
|
+
allowResumeRotation: taskMember ? false : !ledger.resumeDiscarded }, deps);
|
|
1241
1328
|
}
|
|
1242
1329
|
catch (e) {
|
|
1243
1330
|
if (e instanceof SupervisorRecycleRequiredError) {
|
|
@@ -1259,7 +1346,7 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1259
1346
|
mode: 'fresh',
|
|
1260
1347
|
};
|
|
1261
1348
|
}
|
|
1262
|
-
if (stopping)
|
|
1349
|
+
if (stopping || (taskMember && (shouldStop() || result.retirementReason)))
|
|
1263
1350
|
break;
|
|
1264
1351
|
// Re-read: the attempt itself may have taken minutes, and an operator may
|
|
1265
1352
|
// have reset the ledger meanwhile.
|
|
@@ -1277,7 +1364,8 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1277
1364
|
await deps.sleep(HELD_DOWN_POLL_MS);
|
|
1278
1365
|
continue;
|
|
1279
1366
|
}
|
|
1280
|
-
const fastFailSecs =
|
|
1367
|
+
const fastFailSecs = taskMember ? getAdapter(loadTempRole(name).harness).exitPolicy.fastFailSecs
|
|
1368
|
+
: fastFailSecsFor(name, opts.configPath);
|
|
1281
1369
|
// The fast-fail boundary starts a recovery episode; it must not also be
|
|
1282
1370
|
// the boundary that declares recovery successful. Otherwise alternating
|
|
1283
1371
|
// 19s and 20s deaths erase one another forever. Require the configured
|
|
@@ -1314,7 +1402,7 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1314
1402
|
next.nextDelayMs = 0;
|
|
1315
1403
|
writeRestartLedger(dir, next);
|
|
1316
1404
|
deps.log(`[${name}] HELD DOWN after ${failures} immediate failures at ${next.openedAt} — ` +
|
|
1317
|
-
`${reason};
|
|
1405
|
+
`${reason}; recover with: ${taskMember ? `ours-fleet task recover-members ${readTempSupervisor(dir).taskOwner.taskId}` : `ours-fleet restart ${name}`}`);
|
|
1318
1406
|
continue;
|
|
1319
1407
|
}
|
|
1320
1408
|
writeRestartLedger(dir, next);
|
|
@@ -1329,7 +1417,7 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1329
1417
|
process.off('SIGINT', requestStop);
|
|
1330
1418
|
// An orderly shutdown clears the marker; an unhandled signal or OOM-kill
|
|
1331
1419
|
// leaves it so the successor can identify an abrupt termination.
|
|
1332
|
-
if (shouldStop())
|
|
1420
|
+
if (!taskMember && shouldStop())
|
|
1333
1421
|
await deps.releaseAgentOurs(findRole(loadConfig(opts.configPath), name));
|
|
1334
1422
|
releaseSupervisorRun(dir);
|
|
1335
1423
|
}
|
|
@@ -1349,8 +1437,58 @@ function fastFailSecsFor(name, configPath) {
|
|
|
1349
1437
|
}
|
|
1350
1438
|
}
|
|
1351
1439
|
/** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
|
|
1440
|
+
/** The parent uses the same adapter-relative sustained-uptime reset boundary. */
|
|
1441
|
+
export function stableSupervisorWindow(name, temp, configPath) {
|
|
1442
|
+
try {
|
|
1443
|
+
const role = temp ? loadTempRole(name) : findRole(loadConfig(configPath), name);
|
|
1444
|
+
return getAdapter(role.harness).exitPolicy.fastFailSecs * RESTART_FAIL_THRESHOLD;
|
|
1445
|
+
}
|
|
1446
|
+
catch {
|
|
1447
|
+
return 20 * RESTART_FAIL_THRESHOLD;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1352
1450
|
export async function runTemp(name, deps = {}, attempt = runOnce) {
|
|
1353
1451
|
const dir = agentDir(name, true);
|
|
1452
|
+
const metadata = readTempSupervisor(dir);
|
|
1453
|
+
// Non-resumable standalone temporaries retain their prior finite lifecycle:
|
|
1454
|
+
// a parent stop retires them rather than promising retained conversation.
|
|
1455
|
+
if (!metadata?.taskOwner && deps.suspendOnStop && !getAdapter(loadTempRole(name).harness).supportsResume)
|
|
1456
|
+
deps = { ...deps, suspendOnStop: false };
|
|
1457
|
+
if (!metadata?.taskOwner) {
|
|
1458
|
+
const role = loadTempRole(name);
|
|
1459
|
+
const provenance = readProvenance(dir);
|
|
1460
|
+
if (role.roomMemberStartup?.task_id
|
|
1461
|
+
|| (name.startsWith('layout-') && provenance?.creationActionId?.startsWith('task-')))
|
|
1462
|
+
throw Error('TASK_SUPERVISOR_OWNER_MISSING');
|
|
1463
|
+
}
|
|
1464
|
+
if (metadata?.taskOwner) {
|
|
1465
|
+
// Serialize the entire process lifetime before metadata/session writes.
|
|
1466
|
+
const lease = await acquireOwnerBinderLease(join(dir, '.task-supervisor'), 'task-supervisor', name);
|
|
1467
|
+
let ownershipVerified = false;
|
|
1468
|
+
try {
|
|
1469
|
+
const mayRun = taskSupervisorMayRun(name);
|
|
1470
|
+
ownershipVerified = true;
|
|
1471
|
+
if (!mayRun)
|
|
1472
|
+
return;
|
|
1473
|
+
await markTempSupervisorActive(dir);
|
|
1474
|
+
await runSupervised(name, { temp: true }, deps, attempt);
|
|
1475
|
+
}
|
|
1476
|
+
finally {
|
|
1477
|
+
try {
|
|
1478
|
+
if (ownershipVerified && requestedTempStopReason(dir)) {
|
|
1479
|
+
// Parent shutdown suspends; an explicit stop retires the exact saved
|
|
1480
|
+
// owner even when no harness attempt ran (for example held circuits).
|
|
1481
|
+
await (deps.releaseAgentOurs ?? releaseManagedAgent)(loadTempRole(name), taskMemberRetirementProof(name));
|
|
1482
|
+
const archived = archiveTempState(name, 'operator-stop', 'retired', 'explicit task member retirement');
|
|
1483
|
+
deps.log?.(`[${name}] task member retired: operator-stop${archived ? `; evidence archived at ${archived}` : ''}`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
finally {
|
|
1487
|
+
lease.release();
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1354
1492
|
await markTempSupervisorActive(dir);
|
|
1355
1493
|
let signal;
|
|
1356
1494
|
const onTerm = () => { signal = 'SIGTERM'; };
|
|
@@ -1377,7 +1515,8 @@ export async function runTemp(name, deps = {}, attempt = runOnce) {
|
|
|
1377
1515
|
process.off('SIGTERM', onTerm);
|
|
1378
1516
|
process.off('SIGINT', onInt);
|
|
1379
1517
|
// A recycle replaces the supervisor process, not the logical temporary agent.
|
|
1380
|
-
if (!(
|
|
1518
|
+
if (!((signal || deps.shouldStop?.()) && deps.suspendOnStop && !requestedTempStopReason(dir))
|
|
1519
|
+
&& (!(failure instanceof SupervisorRecycleRequiredError) || signal || deps.shouldStop?.())) {
|
|
1381
1520
|
const requested = requestedTempStopReason(dir);
|
|
1382
1521
|
const reason = requested
|
|
1383
1522
|
?? result?.retirementReason
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FileDeliveryInput, CopyChatFile, DeliveredFile } from '../file-delivery/types.js';
|
|
1
2
|
import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
|
|
2
3
|
import * as acp from '@agentclientprotocol/sdk';
|
|
3
4
|
import type { CommonPermissions } from '../config.js';
|
|
@@ -50,6 +51,7 @@ export interface AcpSessionOptions {
|
|
|
50
51
|
inheritEnvironment?: boolean;
|
|
51
52
|
stateDir: string;
|
|
52
53
|
mode: 'fresh' | 'resume';
|
|
54
|
+
requireResume?: boolean;
|
|
53
55
|
permissions: CommonPermissions;
|
|
54
56
|
/** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
|
|
55
57
|
modeId?: string;
|
|
@@ -176,6 +178,7 @@ export declare class AcpSession implements AgentSession {
|
|
|
176
178
|
private stallTimer?;
|
|
177
179
|
private stallAttempt?;
|
|
178
180
|
private readonly toolBoundaryWaiters;
|
|
181
|
+
private fileExportBusy;
|
|
179
182
|
private activeTurn?;
|
|
180
183
|
private constructor();
|
|
181
184
|
static start(options: AcpSessionOptions): Promise<AcpSession>;
|
|
@@ -303,6 +306,8 @@ export declare class AcpSession implements AgentSession {
|
|
|
303
306
|
*/
|
|
304
307
|
private settlePendingAutomatically;
|
|
305
308
|
exitResult(): ExitRecord | null;
|
|
309
|
+
/** One attempt, tied to the managed live turn captured before any approval/read. */
|
|
310
|
+
sendFileToChat(input: FileDeliveryInput, signal: AbortSignal, copy: CopyChatFile, policy: CommonPermissions): Promise<DeliveredFile>;
|
|
306
311
|
close(): Promise<void>;
|
|
307
312
|
/** ACP requires the field. Bundled agents treat [] as no client-added servers. */
|
|
308
313
|
private declaredMcpServers;
|
package/dist/session/acp.js
CHANGED
|
@@ -278,6 +278,7 @@ export class AcpSession {
|
|
|
278
278
|
stallTimer;
|
|
279
279
|
stallAttempt;
|
|
280
280
|
toolBoundaryWaiters = new Set();
|
|
281
|
+
fileExportBusy = false;
|
|
281
282
|
activeTurn;
|
|
282
283
|
constructor(options, child, connection) {
|
|
283
284
|
this.options = options;
|
|
@@ -466,6 +467,7 @@ export class AcpSession {
|
|
|
466
467
|
readiness: this.readiness === 'idle' && this.steeringOccupied
|
|
467
468
|
? 'running' : this.readiness,
|
|
468
469
|
sessionId: this.sessionId,
|
|
470
|
+
...(this.activeTurn?.origin ? { activePromptSource: this.activeTurn.origin.kind } : {}),
|
|
469
471
|
lastError: this.lastError,
|
|
470
472
|
pendingPermissionId: this.pendingPermissions.keys().next().value,
|
|
471
473
|
runtimeModel: this.runtimeModel,
|
|
@@ -580,6 +582,10 @@ export class AcpSession {
|
|
|
580
582
|
* steering response from becoming a tight replay loop.
|
|
581
583
|
*/
|
|
582
584
|
async steerOrQueueWake(text, options) {
|
|
585
|
+
// A tool-boundary wait may outlive its original turn. Recheck the current
|
|
586
|
+
// typed source at delivery, since an Owner turn can begin during the wait.
|
|
587
|
+
if (options.origin?.kind === 'fleet-monitor' && this.activeTurn?.origin?.kind === 'owner-admin-console')
|
|
588
|
+
return this.submitPrompt(text, { ...options, interrupt: false, steer: false });
|
|
583
589
|
if (this.stallRecoveryClaimed)
|
|
584
590
|
return this.submitPrompt(text, { ...options, interrupt: false, steer: false });
|
|
585
591
|
const steered = await this.steerPrompt(text);
|
|
@@ -823,6 +829,8 @@ export class AcpSession {
|
|
|
823
829
|
if (!this.sessionId)
|
|
824
830
|
return;
|
|
825
831
|
const active = this.activeTurn;
|
|
832
|
+
if (active)
|
|
833
|
+
active.fileDeliveryRevoked = true;
|
|
826
834
|
const previousSource = active?.cancellationSource;
|
|
827
835
|
if (active && (source === 'owner' || source === 'local-console' || !previousSource))
|
|
828
836
|
active.cancellationSource = source;
|
|
@@ -1026,6 +1034,54 @@ export class AcpSession {
|
|
|
1026
1034
|
exitResult() {
|
|
1027
1035
|
return this.exit;
|
|
1028
1036
|
}
|
|
1037
|
+
/** One attempt, tied to the managed live turn captured before any approval/read. */
|
|
1038
|
+
async sendFileToChat(input, signal, copy, policy) {
|
|
1039
|
+
const active = this.activeTurn, sessionId = this.sessionId;
|
|
1040
|
+
const check = () => {
|
|
1041
|
+
signal.throwIfAborted();
|
|
1042
|
+
if (!active || !sessionId || this.activeTurn !== active || this.sessionId !== sessionId || active.fileDeliveryRevoked
|
|
1043
|
+
|| active.cancellationSource || this.closing || !this.isAlive() || this.replaying || this.steeringOccupied
|
|
1044
|
+
|| active.origin?.kind === 'scheduled-loop' || policy.approval === 'deny')
|
|
1045
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE');
|
|
1046
|
+
};
|
|
1047
|
+
check();
|
|
1048
|
+
if (this.fileExportBusy)
|
|
1049
|
+
throw Error('FILE_DELIVERY_BUSY');
|
|
1050
|
+
this.fileExportBusy = true;
|
|
1051
|
+
const toolCallId = 'fleet-file-export-' + randomUUID();
|
|
1052
|
+
try {
|
|
1053
|
+
const onAbort = () => {
|
|
1054
|
+
for (const [id, pending] of this.pendingPermissions)
|
|
1055
|
+
if (pending.toolCallId === toolCallId)
|
|
1056
|
+
this.settlePendingAutomatically(id, pending, 'cancelled', undefined, 'File send cancelled');
|
|
1057
|
+
};
|
|
1058
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1059
|
+
let answer;
|
|
1060
|
+
try {
|
|
1061
|
+
answer = await this.requestPermission({ sessionId: sessionId, toolCall: {
|
|
1062
|
+
toolCallId, title: 'Send file to this ACP chat: ' + (input.filename ?? input.path), kind: 'read', status: 'pending',
|
|
1063
|
+
locations: [{ path: resolve(this.options.cwd, input.path) }],
|
|
1064
|
+
}, options: [{ optionId: 'send', name: 'Send file', kind: 'allow_once' }, { optionId: 'deny', name: 'Do not send', kind: 'reject_once' }] }, policy);
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
signal.removeEventListener('abort', onAbort);
|
|
1068
|
+
}
|
|
1069
|
+
check();
|
|
1070
|
+
if (answer.outcome.outcome !== 'selected' || answer.outcome.optionId !== 'send')
|
|
1071
|
+
throw Error('FILE_SEND_DENIED');
|
|
1072
|
+
const attachment = await copy({ sessionGeneration: this.sessionGeneration, acpSessionId: sessionId, turnId: active.id });
|
|
1073
|
+
// Synchronous check + durable append establishes order with cancellation/stop.
|
|
1074
|
+
// No await, pending-send journal, replay or recovery exists after this point.
|
|
1075
|
+
check();
|
|
1076
|
+
this.conversation.append({ kind: 'file.attached', source: 'agent', sessionGeneration: this.sessionGeneration,
|
|
1077
|
+
acpSessionId: sessionId, promptId: active.id, turnId: active.id, payload: { attachment } });
|
|
1078
|
+
return attachment;
|
|
1079
|
+
}
|
|
1080
|
+
finally {
|
|
1081
|
+
this.releaseTool(toolCallId);
|
|
1082
|
+
this.fileExportBusy = false;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1029
1085
|
async close() {
|
|
1030
1086
|
this.closing = true;
|
|
1031
1087
|
if (this.stallTimer)
|
|
@@ -1079,6 +1135,8 @@ export class AcpSession {
|
|
|
1079
1135
|
const persisted = this.options.mode === 'resume' && existsSync(this.sessionFile)
|
|
1080
1136
|
? readFileSync(this.sessionFile, 'utf8').trim()
|
|
1081
1137
|
: '';
|
|
1138
|
+
if (this.options.requireResume && (!persisted || !(this.agentCapabilities?.sessionCapabilities?.resume != null || this.agentCapabilities?.loadSession)))
|
|
1139
|
+
throw new Error('TASK_CONTEXT_RESUME_UNAVAILABLE');
|
|
1082
1140
|
let advertisedConfigOptions;
|
|
1083
1141
|
let advertisedModes;
|
|
1084
1142
|
let advertisedModelId;
|
|
@@ -1414,6 +1472,8 @@ export class AcpSession {
|
|
|
1414
1472
|
}
|
|
1415
1473
|
async steerPrompt(text) {
|
|
1416
1474
|
this.steeringWasUsed = true;
|
|
1475
|
+
if (this.activeTurn)
|
|
1476
|
+
this.activeTurn.fileDeliveryRevoked = true;
|
|
1417
1477
|
if (!this.sessionId || !this.isAlive())
|
|
1418
1478
|
return turnResult(false, 'failed', this.lastError ?? 'ACP session is offline');
|
|
1419
1479
|
this.steeringRequests++;
|
|
@@ -1445,7 +1505,7 @@ export class AcpSession {
|
|
|
1445
1505
|
this.steeringRequests--;
|
|
1446
1506
|
}
|
|
1447
1507
|
}
|
|
1448
|
-
requestPermission(params) {
|
|
1508
|
+
requestPermission(params, neutralPolicy) {
|
|
1449
1509
|
// `kinds` is a PRIORITY order. Scanning the agent's option array instead
|
|
1450
1510
|
// (`options.find(o => kinds.includes(o.kind))`) hands the choice to whatever
|
|
1451
1511
|
// order the agent happened to list, which is exactly how an automatic denial
|
|
@@ -1463,7 +1523,7 @@ export class AcpSession {
|
|
|
1463
1523
|
// Permission is part of the tool lifecycle. Reserve before any policy or
|
|
1464
1524
|
// human decision so a monitor wake cannot slip between request and answer.
|
|
1465
1525
|
this.reservePermission(toolCallId, permissionId);
|
|
1466
|
-
if (this.isEffectiveCodexProtectedMcpApproval(params)) {
|
|
1526
|
+
if (!neutralPolicy && this.isEffectiveCodexProtectedMcpApproval(params)) {
|
|
1467
1527
|
// Protected MCP approval is already the tool's narrow gate. Never turn
|
|
1468
1528
|
// this one decision into an adapter-wide standing grant.
|
|
1469
1529
|
const option = choose(['allow_once']);
|
|
@@ -1474,9 +1534,9 @@ export class AcpSession {
|
|
|
1474
1534
|
this.releasePermission(toolCallId, permissionId);
|
|
1475
1535
|
return Promise.resolve(response);
|
|
1476
1536
|
}
|
|
1477
|
-
if (this.options.permissions.approval === 'allow' && this.withinAutomaticBoundary(params)) {
|
|
1537
|
+
if ((neutralPolicy ?? this.options.permissions).approval === 'allow' && this.withinAutomaticBoundary(params, neutralPolicy)) {
|
|
1478
1538
|
const option = choose(['allow_always', 'allow_once']);
|
|
1479
|
-
const response = this.settleAutomatically(params, option, 'allowed', 'permissions.approval=allow', `the request is inside the ${this.options.permissions.filesystem} boundary`);
|
|
1539
|
+
const response = this.settleAutomatically(params, option, 'allowed', 'permissions.approval=allow', `the request is inside the ${(neutralPolicy ?? this.options.permissions).filesystem} boundary`);
|
|
1480
1540
|
if (option)
|
|
1481
1541
|
this.allowPermission(toolCallId, permissionId);
|
|
1482
1542
|
else
|
|
@@ -1486,8 +1546,8 @@ export class AcpSession {
|
|
|
1486
1546
|
// A live grace window still counts as attended: the controller may be
|
|
1487
1547
|
// mid-reconnect, and denying instantly is exactly what the grace prevents.
|
|
1488
1548
|
const unattended = this.controllerCount === 0 && !this.controllerGrace
|
|
1489
|
-
&& this.options.permissions.unattended === 'deny';
|
|
1490
|
-
if (this.options.permissions.approval === 'deny' || unattended) {
|
|
1549
|
+
&& (neutralPolicy ?? this.options.permissions).unattended === 'deny';
|
|
1550
|
+
if ((neutralPolicy ?? this.options.permissions).approval === 'deny' || unattended) {
|
|
1491
1551
|
// reject_once FIRST: `reject_always` teaches the agent a standing rule from
|
|
1492
1552
|
// a decision no human made, so one unattended denial would silently disable
|
|
1493
1553
|
// the tool for the rest of the session.
|
|
@@ -1588,8 +1648,8 @@ export class AcpSession {
|
|
|
1588
1648
|
? { outcome: { outcome: 'selected', optionId: option.optionId } }
|
|
1589
1649
|
: { outcome: { outcome: 'cancelled' } };
|
|
1590
1650
|
}
|
|
1591
|
-
withinAutomaticBoundary(params) {
|
|
1592
|
-
const filesystem = this.options.permissions.filesystem;
|
|
1651
|
+
withinAutomaticBoundary(params, policy) {
|
|
1652
|
+
const filesystem = (policy ?? this.options.permissions).filesystem;
|
|
1593
1653
|
if (filesystem === 'unrestricted')
|
|
1594
1654
|
return true;
|
|
1595
1655
|
if (filesystem === 'read-only' && params.toolCall.kind !== 'read')
|