@ours.network/fleet 1.2.0-nightly.40 → 1.2.0-nightly.42
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
|
-
import { loadConfig,
|
|
4
|
+
import { loadConfig, resolveSnapshotMonitor, resolvePermissions, ROLE_NAME_RE, } from '../config.js';
|
|
5
5
|
import { agentsRoot, tmpRoot } from '../paths.js';
|
|
6
6
|
import { readSpawnLineage } from '../web/topology.js';
|
|
7
7
|
const MAX_SNAPSHOT_BYTES = 256 * 1024;
|
|
@@ -54,7 +54,7 @@ function snapshot(path, name) {
|
|
|
54
54
|
session, identity: typeof raw.identity === 'string' ? raw.identity : name,
|
|
55
55
|
permissions: resolvePermissions(undefined, raw.permissions),
|
|
56
56
|
permissionsDeclared: raw.permissions !== undefined,
|
|
57
|
-
monitor:
|
|
57
|
+
monitor: resolveSnapshotMonitor(raw.monitor),
|
|
58
58
|
sourceFile: '(temp)',
|
|
59
59
|
},
|
|
60
60
|
};
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.42",
|
|
3
|
+
"buildId": "536c9cc8b8d7",
|
|
4
|
+
"commit": "c51b38eaa547d240e97f776e09286590222e727e",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-03T20:14:35.466Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"workspace.enroll.preserve-profile-v1",
|
|
9
9
|
"cowork.http-management-v1",
|
package/dist/config.d.ts
CHANGED
|
@@ -343,6 +343,11 @@ export declare function resolvePermissions(defaults: unknown, role: Partial<Comm
|
|
|
343
343
|
* without tool-boundary steering fall back to non-cancelling delivery.
|
|
344
344
|
*/
|
|
345
345
|
export declare function defaultMonitorInterrupt(harness: string | undefined): MonitorInterrupt;
|
|
346
|
+
/**
|
|
347
|
+
* Resolve a persisted temporary-role monitor. A snapshot written before
|
|
348
|
+
* monitor.interrupt existed keeps the non-interrupting delivery it ran with.
|
|
349
|
+
*/
|
|
350
|
+
export declare function resolveSnapshotMonitor(monitor: unknown): MonitorConfig;
|
|
346
351
|
export declare function resolveMonitorConfig(defMonitor: unknown, roleMonitor?: Partial<MonitorConfig>, labels?: {
|
|
347
352
|
base?: string;
|
|
348
353
|
file?: string;
|
package/dist/config.js
CHANGED
|
@@ -1150,6 +1150,15 @@ function resolveStartStaggerMs(raw, base) {
|
|
|
1150
1150
|
export function defaultMonitorInterrupt(harness) {
|
|
1151
1151
|
return harness === 'hermes' ? false : 'after_tool';
|
|
1152
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Resolve a persisted temporary-role monitor. A snapshot written before
|
|
1155
|
+
* monitor.interrupt existed keeps the non-interrupting delivery it ran with.
|
|
1156
|
+
*/
|
|
1157
|
+
export function resolveSnapshotMonitor(monitor) {
|
|
1158
|
+
if (!isPlainObject(monitor))
|
|
1159
|
+
return resolveMonitorConfig(undefined, monitor);
|
|
1160
|
+
return resolveMonitorConfig(undefined, { interrupt: false, ...monitor });
|
|
1161
|
+
}
|
|
1153
1162
|
export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
1154
1163
|
const where = labels.file && labels.name ? `${labels.file}: role '${labels.name}' ` : '';
|
|
1155
1164
|
if (defMonitor !== undefined && !isPlainObject(defMonitor))
|
|
@@ -45,9 +45,9 @@ export async function requestMemberAddition(input) {
|
|
|
45
45
|
}
|
|
46
46
|
const task = getTask(taskId);
|
|
47
47
|
const room = task.room_id && getRoomRecord(task.room_id);
|
|
48
|
-
if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent ||
|
|
48
|
+
if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || room.state !== 'active'
|
|
49
49
|
|| room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
|
|
50
|
-
throw new MemberAdditionRejected('Adding an agent requires an active
|
|
50
|
+
throw new MemberAdditionRejected('Adding an agent requires an active task room');
|
|
51
51
|
const roleId = `${taskId.slice(0, 8)}-${r.slot}-1`;
|
|
52
52
|
if (room.member_seats.some(s => s.role_name === roleId))
|
|
53
53
|
throw new MemberAdditionRejected('This member slot already exists');
|
|
@@ -379,9 +379,9 @@ export async function provisionMembers(input) {
|
|
|
379
379
|
if (input.append) {
|
|
380
380
|
const task = input.taskId ? getTask(input.taskId) : undefined;
|
|
381
381
|
const room = getRoomRecord(input.roomId);
|
|
382
|
-
if (!task || !['active', 'review'].includes(task.state) ||
|
|
382
|
+
if (!task || !['active', 'review'].includes(task.state) || room?.state !== 'active'
|
|
383
383
|
|| room.task_id !== task.task_id || room.room_identity_cid !== task.room_identity_cid)
|
|
384
|
-
throw new Error('Adding an agent requires the exact active
|
|
384
|
+
throw new Error('Adding an agent requires the exact active task room');
|
|
385
385
|
const names = expandMembers(input.template, shortId(task.task_id)).map(m => m.name);
|
|
386
386
|
if (room.member_seats.some(seat => names.includes(seat.role_name)))
|
|
387
387
|
throw new Error('Member slot is already recorded; inspect its existing launch');
|
package/dist/runner.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { parse } from 'yaml';
|
|
10
10
|
import { agentDir, stateRoot } from './paths.js';
|
|
11
|
-
import { loadConfig, findRole, isolationContextFor,
|
|
11
|
+
import { loadConfig, findRole, isolationContextFor, resolveSnapshotMonitor, resolvePermissions, } from './config.js';
|
|
12
12
|
import { getAdapter } from './harness/registry.js';
|
|
13
13
|
import { createMonitor, probeIdentityPresence, } from './monitor.js';
|
|
14
14
|
import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
|
|
@@ -432,12 +432,9 @@ export function loadTempRole(name) {
|
|
|
432
432
|
throw new Error(`temp role '${name}' has no snapshot at ${p}`);
|
|
433
433
|
const role = parse(readFileSync(p, 'utf8'));
|
|
434
434
|
// Upgrade snapshots written before monitor.mode/interrupt existed. A snapshot
|
|
435
|
-
// with no monitor block keeps the historical native/no-supervisor behavior
|
|
436
|
-
// and one without interrupt keeps the non-interrupting delivery it ran with.
|
|
435
|
+
// with no monitor block keeps the historical native/no-supervisor behavior.
|
|
437
436
|
if (role.monitor)
|
|
438
|
-
role.monitor =
|
|
439
|
-
interrupt: false, ...role.monitor,
|
|
440
|
-
});
|
|
437
|
+
role.monitor = resolveSnapshotMonitor(role.monitor);
|
|
441
438
|
role.__temp = true;
|
|
442
439
|
return role;
|
|
443
440
|
}
|
|
@@ -629,6 +626,16 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
629
626
|
let unsubscribeRecovery;
|
|
630
627
|
let monitorLoop;
|
|
631
628
|
let sessionStartupComplete = false;
|
|
629
|
+
let startupRecoveryAllowed = false;
|
|
630
|
+
let successfulTurnObserved = false;
|
|
631
|
+
let monitorMayStart = false;
|
|
632
|
+
let monitorStarted = false;
|
|
633
|
+
const startMonitorAfterStartup = () => {
|
|
634
|
+
if (!monitorStarted && monitorMayStart && sessionStartupComplete && !deps.shouldStop?.()) {
|
|
635
|
+
monitorStarted = true;
|
|
636
|
+
monitorLoop = monitor?.run(pid);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
632
639
|
let sessionClosed = false;
|
|
633
640
|
let ownerChannel;
|
|
634
641
|
let ownerBinder;
|
|
@@ -657,13 +664,23 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
657
664
|
pid = agentSession.pid;
|
|
658
665
|
arbiter = new RoleTurnArbiter(agentSession);
|
|
659
666
|
sessionHandle = arbiter;
|
|
660
|
-
|
|
667
|
+
const unsubscribeFailure = agentSession.subscribe(event => {
|
|
661
668
|
if (event.kind !== 'error' || !event.text || event.origin?.kind === 'stall-watchdog')
|
|
662
669
|
return;
|
|
663
670
|
const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
|
|
664
671
|
if (evidence)
|
|
665
672
|
resolvedMonitorDeps.onFailureEvidence?.(evidence);
|
|
666
673
|
});
|
|
674
|
+
const unsubscribeStartup = agentSession.subscribeConversation?.(event => {
|
|
675
|
+
if (event.kind !== 'turn.completed' || !('outcome' in event.payload) || event.payload.outcome !== 'completed')
|
|
676
|
+
return;
|
|
677
|
+
successfulTurnObserved = true;
|
|
678
|
+
if (startupRecoveryAllowed) {
|
|
679
|
+
sessionStartupComplete = true;
|
|
680
|
+
startMonitorAfterStartup();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
unsubscribeRecovery = () => { unsubscribeFailure(); unsubscribeStartup?.(); };
|
|
667
684
|
if (role.owner_channel) {
|
|
668
685
|
try {
|
|
669
686
|
ownerBinder = await deps.acquireOwnerBinder(dir, name, role.owner_channel.identity);
|
|
@@ -742,10 +759,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
742
759
|
// refusal or a cancellation reached the agent and was not acted on, so
|
|
743
760
|
// the monitor must keep its cursor and try again.
|
|
744
761
|
submit: async (text, options) => {
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
// steer into the live turn instead; after it completes, honor the
|
|
748
|
-
// configured interrupt policy normally.
|
|
762
|
+
// Delivery starts after the protected startup turn succeeds. Honor the
|
|
763
|
+
// configured interrupt policy for subsequent turns.
|
|
749
764
|
const policy = options?.interrupt;
|
|
750
765
|
const interrupt = policy === true && sessionStartupComplete;
|
|
751
766
|
const promptOptions = {
|
|
@@ -753,8 +768,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
753
768
|
...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
|
|
754
769
|
origin: { kind: 'fleet-monitor' },
|
|
755
770
|
};
|
|
756
|
-
//
|
|
757
|
-
// steer rather than waiting on/cancelling the runner-owned first turn.
|
|
771
|
+
// after_tool waits for a safe boundary in subsequent active turns.
|
|
758
772
|
const result = policy === 'after_tool' && sessionStartupComplete
|
|
759
773
|
? await arbiter.submitPromptAfterTool(text, promptOptions)
|
|
760
774
|
: await arbiter.submitPrompt(text, promptOptions);
|
|
@@ -790,10 +804,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
790
804
|
// startup prompt and then refuses it has not started; logging the role as
|
|
791
805
|
// up would hide a role that never read its briefing.
|
|
792
806
|
const starting = arbiter.submitPrompt(firstPrompt, { origin: { kind: 'startup' } });
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
// success, so there is neither a deaf gap nor a boot-cancellation loop.
|
|
796
|
-
monitorLoop = monitor?.run(pid);
|
|
807
|
+
// Keep the early cursor baseline; do not deliver wakes into the protected
|
|
808
|
+
// first turn. Steering can pre-empt startup just like cancellation.
|
|
797
809
|
// Owner traffic and agent replies must be observed during a long first
|
|
798
810
|
// turn. Ordinary owner input steers until startup has proved successful.
|
|
799
811
|
if (ownerChannel) {
|
|
@@ -853,7 +865,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
853
865
|
else if (interruptedForWake)
|
|
854
866
|
deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
|
|
855
867
|
+ 'keeping temporary supervisor alive');
|
|
856
|
-
|
|
868
|
+
startupRecoveryAllowed = interruptedForWake;
|
|
869
|
+
sessionStartupComplete = started.succeeded || (interruptedForWake && successfulTurnObserved);
|
|
857
870
|
reloadLoopConfig = async () => {
|
|
858
871
|
const nextRole = findRole(loadConfig(configPath), name);
|
|
859
872
|
const definitions = nextRole.loops ?? [];
|
|
@@ -910,9 +923,10 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
910
923
|
}
|
|
911
924
|
}
|
|
912
925
|
deps.log(`[${name}] up; pid=${pid} cwd=${runCwd} harness=${role.harness} session=${sessionBackend} mode=${mode}`);
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
|
|
926
|
+
// Keep arrivals behind the early prime() cursor; never re-prime at startup.
|
|
927
|
+
// Recoverable cancellation preserves the supervisor, not monitor readiness.
|
|
928
|
+
monitorMayStart = true;
|
|
929
|
+
startMonitorAfterStartup();
|
|
916
930
|
recoveryController = new RoleRecoveryController({
|
|
917
931
|
role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
|
|
918
932
|
log: deps.log,
|
|
@@ -1031,6 +1045,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1031
1045
|
}
|
|
1032
1046
|
}
|
|
1033
1047
|
}
|
|
1048
|
+
monitorMayStart = false;
|
|
1034
1049
|
if (loopManager) {
|
|
1035
1050
|
control?.setLoopManager(undefined);
|
|
1036
1051
|
await loopManager.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.42",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|