@ours.network/fleet 0.17.5 → 0.17.6
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/dist/application/fleet-query-service.js +12 -0
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +16 -3
- package/dist/session/acp.d.ts +6 -0
- package/dist/session/acp.js +14 -0
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/types.d.ts +24 -0
- package/dist/watchdog/briefing.js +7 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { classifyActivity } from '../session/activity.js';
|
|
3
4
|
import { controlRequest } from '../session/control.js';
|
|
4
5
|
import { SessionControlError } from '../session/types.js';
|
|
5
6
|
import { readExitRecord, readRestartLedger } from '../runner.js';
|
|
@@ -55,6 +56,10 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
|
|
|
55
56
|
if (session.reachability === 'online'
|
|
56
57
|
&& (session.readiness === 'running' || session.readiness === 'awaiting_permission'))
|
|
57
58
|
return 'busy';
|
|
59
|
+
// `readiness: idle` alone never justifies `ready`. A steered wake turn is
|
|
60
|
+
// invisible to readiness (FLEET-002), so observed activity outranks it.
|
|
61
|
+
if (session.reachability === 'online' && session.activity.state === 'active')
|
|
62
|
+
return 'busy';
|
|
58
63
|
if (session.reachability === 'online' && session.readiness === 'idle')
|
|
59
64
|
return 'ready';
|
|
60
65
|
if (session.reachability === 'offline'
|
|
@@ -153,6 +158,7 @@ export class FleetQueryService {
|
|
|
153
158
|
protocolVersion: snapshot.protocolVersion, features: snapshot.features,
|
|
154
159
|
runtimeModel: snapshot.runtimeModel, reasoningEffort: snapshot.reasoningEffort,
|
|
155
160
|
permissionMode: snapshot.permissionMode,
|
|
161
|
+
activity: classifyActivity(snapshot.activity),
|
|
156
162
|
};
|
|
157
163
|
}
|
|
158
164
|
catch (error) {
|
|
@@ -164,6 +170,7 @@ export class FleetQueryService {
|
|
|
164
170
|
: failure === 'control-unavailable' ? 'unavailable' : 'unknown',
|
|
165
171
|
readiness: offline ? 'failed' : 'unknown',
|
|
166
172
|
evidence: 'authoritative', lastError: clean(error.message),
|
|
173
|
+
activity: { state: 'unobservable' },
|
|
167
174
|
};
|
|
168
175
|
}
|
|
169
176
|
}
|
|
@@ -174,18 +181,23 @@ export class FleetQueryService {
|
|
|
174
181
|
backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
|
|
175
182
|
readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
|
|
176
183
|
evidence: 'inferred',
|
|
184
|
+
// tmux exposes no agent-side evidence at all, and `readiness: idle`
|
|
185
|
+
// here is a pane-liveness inference, not an activity claim.
|
|
186
|
+
activity: { state: 'unobservable' },
|
|
177
187
|
};
|
|
178
188
|
}
|
|
179
189
|
catch (error) {
|
|
180
190
|
return {
|
|
181
191
|
backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
|
|
182
192
|
evidence: 'inferred', lastError: clean(error.message),
|
|
193
|
+
activity: { state: 'unobservable' },
|
|
183
194
|
};
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
return {
|
|
187
198
|
backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
|
|
188
199
|
readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
|
|
200
|
+
activity: { state: 'unobservable' },
|
|
189
201
|
};
|
|
190
202
|
}
|
|
191
203
|
}
|
|
@@ -72,6 +72,17 @@ export interface RoleStatus {
|
|
|
72
72
|
runtimeModel?: SessionSnapshot['runtimeModel'];
|
|
73
73
|
reasoningEffort?: SessionSnapshot['reasoningEffort'];
|
|
74
74
|
permissionMode?: SessionSnapshot['permissionMode'];
|
|
75
|
+
/**
|
|
76
|
+
* Activity evidence, kept separate from `readiness` on purpose: `readiness`
|
|
77
|
+
* answers "is a fleet-tracked turn in flight" (the prompt-admission gate),
|
|
78
|
+
* NOT "is this agent working". `state` is the only field a human-facing
|
|
79
|
+
* surface may use to call a role idle or stalled.
|
|
80
|
+
*/
|
|
81
|
+
activity: {
|
|
82
|
+
state: 'active' | 'quiet' | 'unobservable';
|
|
83
|
+
activeToolCalls?: number;
|
|
84
|
+
lastUpdateAt?: string;
|
|
85
|
+
};
|
|
75
86
|
};
|
|
76
87
|
restart: {
|
|
77
88
|
circuit: 'closed' | 'open';
|
package/dist/briefing.js
CHANGED
|
@@ -168,6 +168,11 @@ export function generateBriefing(role, v, opts) {
|
|
|
168
168
|
L.push('Never translate any other failure into "dead". A busy agent, an unanswered control');
|
|
169
169
|
L.push('plane and a confirmed stop look identical if you only look at one command.');
|
|
170
170
|
L.push('');
|
|
171
|
+
L.push('`ours-fleet status` reports `session.readiness`, which is TURN OCCUPANCY only: a mail');
|
|
172
|
+
L.push('wake delivered by steering runs a whole turn with readiness pinned at `idle`. Read the');
|
|
173
|
+
L.push('`activity:` line beside it — `active` means the role is working — and never call a role');
|
|
174
|
+
L.push('idle or stalled from `readiness=idle` alone.');
|
|
175
|
+
L.push('');
|
|
171
176
|
L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
|
|
172
177
|
L.push('with `ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); idle with work');
|
|
173
178
|
L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.17.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "0.17.6",
|
|
3
|
+
"buildId": "a2f7650e3a39",
|
|
4
|
+
"commit": "96df9112202bb3957d18d95bc4210c1cf49d867f",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-17T11:51:49.146Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { creationBuildNote, formatProvenance, readProvenance } from './creation.
|
|
|
27
27
|
import { doctor } from './doctor.js';
|
|
28
28
|
import { allWarnings, analyzeFleetPermissions, effectivePermissionMode, formatNative, } from './permissions.js';
|
|
29
29
|
import { AI_DOCS } from './docs.js';
|
|
30
|
+
import { classifyActivity, describeSessionState } from './session/activity.js';
|
|
30
31
|
import { controlRequest, controlSocketPath, followControl, livenessNote, } from './session/control.js';
|
|
31
32
|
import { SessionControlError } from './session/types.js';
|
|
32
33
|
import { readScheduledLoops, storedLoopHealth } from './loops/state.js';
|
|
@@ -363,8 +364,14 @@ program.command('ls').description('list running fleet sessions')
|
|
|
363
364
|
continue;
|
|
364
365
|
try {
|
|
365
366
|
const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
|
|
366
|
-
|
|
367
|
-
|
|
367
|
+
const result = response.result;
|
|
368
|
+
if (response.ok && result?.alive) {
|
|
369
|
+
// Activity, not readiness: an idle-readiness role may be running a
|
|
370
|
+
// steered wake turn (FLEET-002), so `ls` reports what was observed.
|
|
371
|
+
const observed = classifyActivity(result.activity);
|
|
372
|
+
acp.push(`${name}: acp${observed.state === 'active' ? ' (working)'
|
|
373
|
+
: observed.state === 'quiet' ? ' (no recent agent activity)' : ''}`);
|
|
374
|
+
}
|
|
368
375
|
}
|
|
369
376
|
catch { /* ignore stale sockets */ }
|
|
370
377
|
}
|
|
@@ -523,8 +530,14 @@ program.command('status <name>').description('unit/agent state')
|
|
|
523
530
|
if (stateDir) {
|
|
524
531
|
try {
|
|
525
532
|
const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
|
|
526
|
-
if (response.ok)
|
|
533
|
+
if (response.ok) {
|
|
534
|
+
const snapshot = response.result;
|
|
527
535
|
console.log(`session: ${JSON.stringify(response.result)}`);
|
|
536
|
+
// `readiness` is turn occupancy, never an activity claim: a steered
|
|
537
|
+
// wake turn runs to completion with readiness pinned at `idle`
|
|
538
|
+
// (FLEET-002). Say which question each field answers.
|
|
539
|
+
console.log(describeSessionState(snapshot.readiness, snapshot.activity));
|
|
540
|
+
}
|
|
528
541
|
}
|
|
529
542
|
catch {
|
|
530
543
|
console.log('session: acp control unavailable');
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export declare class AcpSession implements SessionHandle {
|
|
|
60
60
|
private connection;
|
|
61
61
|
private sessionId?;
|
|
62
62
|
private readiness;
|
|
63
|
+
/**
|
|
64
|
+
* Last non-replayed session update from the agent. `readiness` cannot answer
|
|
65
|
+
* "is this agent working" for a steered turn (FLEET-002), and this is the
|
|
66
|
+
* evidence that can.
|
|
67
|
+
*/
|
|
68
|
+
private lastUpdateAt?;
|
|
63
69
|
private lastError?;
|
|
64
70
|
private promptTail;
|
|
65
71
|
private queueDepth;
|
package/dist/session/acp.js
CHANGED
|
@@ -202,6 +202,12 @@ export class AcpSession {
|
|
|
202
202
|
connection;
|
|
203
203
|
sessionId;
|
|
204
204
|
readiness = 'starting';
|
|
205
|
+
/**
|
|
206
|
+
* Last non-replayed session update from the agent. `readiness` cannot answer
|
|
207
|
+
* "is this agent working" for a steered turn (FLEET-002), and this is the
|
|
208
|
+
* evidence that can.
|
|
209
|
+
*/
|
|
210
|
+
lastUpdateAt;
|
|
205
211
|
lastError;
|
|
206
212
|
promptTail = Promise.resolve();
|
|
207
213
|
queueDepth = 0;
|
|
@@ -358,6 +364,10 @@ export class AcpSession {
|
|
|
358
364
|
runtimeModel: this.runtimeModel,
|
|
359
365
|
reasoningEffort: this.reasoningEffort,
|
|
360
366
|
permissionMode: this.options.permissionMode,
|
|
367
|
+
activity: {
|
|
368
|
+
activeToolCalls: this.activeToolCalls.size,
|
|
369
|
+
...(this.lastUpdateAt ? { lastUpdateAt: this.lastUpdateAt } : {}),
|
|
370
|
+
},
|
|
361
371
|
};
|
|
362
372
|
}
|
|
363
373
|
toolCall(toolCallId) {
|
|
@@ -1219,6 +1229,10 @@ export class AcpSession {
|
|
|
1219
1229
|
&& params.options.some(option => option.optionId === 'decline' && option.kind === 'reject_once');
|
|
1220
1230
|
}
|
|
1221
1231
|
recordUpdate(update) {
|
|
1232
|
+
// Replayed history is not current activity: `session/load` would otherwise
|
|
1233
|
+
// make a cold session look like it had just been working.
|
|
1234
|
+
if (!this.replaying)
|
|
1235
|
+
this.lastUpdateAt = new Date().toISOString();
|
|
1222
1236
|
const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
|
|
1223
1237
|
const messagePhase = update.sessionUpdate === 'agent_message_chunk'
|
|
1224
1238
|
? this.codexMessagePhase(update) : undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SessionActivity } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* How long after the agent's last session update it still counts as working.
|
|
4
|
+
*
|
|
5
|
+
* FLEET-002: a wake delivered by ACP steering answers `startedNewTurn` and runs
|
|
6
|
+
* an entire turn that fleet never receives a `session/prompt` response for (ACP
|
|
7
|
+
* has no turn-end session update), so `readiness` stays `idle` throughout. Tool
|
|
8
|
+
* reservations and update recency are the only activity evidence fleet holds.
|
|
9
|
+
* The trade-off is deliberate and one-directional: at worst a role reads busy
|
|
10
|
+
* for one window after it genuinely stopped, instead of reading ready — or
|
|
11
|
+
* being classified stalled — while it is executing tools.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ACTIVITY_WINDOW_MS = 60000;
|
|
14
|
+
export type ActivityState = 'active' | 'quiet' | 'unobservable';
|
|
15
|
+
export interface ObservedActivity {
|
|
16
|
+
state: ActivityState;
|
|
17
|
+
activeToolCalls?: number;
|
|
18
|
+
lastUpdateAt?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify agent-side activity. `unobservable` is NOT `quiet`: a backend that
|
|
22
|
+
* cannot see the agent (tmux) has no evidence, and no evidence must never be
|
|
23
|
+
* reported as "doing nothing".
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyActivity(activity: SessionActivity | undefined, now?: number): ObservedActivity;
|
|
26
|
+
/**
|
|
27
|
+
* One operator-facing line that never lets turn occupancy pose as liveness:
|
|
28
|
+
* the readiness value is labelled as the turn field it is, and the activity
|
|
29
|
+
* verdict is stated separately with the evidence behind it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeSessionState(readiness: string | undefined, activity: SessionActivity | undefined, now?: number): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long after the agent's last session update it still counts as working.
|
|
3
|
+
*
|
|
4
|
+
* FLEET-002: a wake delivered by ACP steering answers `startedNewTurn` and runs
|
|
5
|
+
* an entire turn that fleet never receives a `session/prompt` response for (ACP
|
|
6
|
+
* has no turn-end session update), so `readiness` stays `idle` throughout. Tool
|
|
7
|
+
* reservations and update recency are the only activity evidence fleet holds.
|
|
8
|
+
* The trade-off is deliberate and one-directional: at worst a role reads busy
|
|
9
|
+
* for one window after it genuinely stopped, instead of reading ready — or
|
|
10
|
+
* being classified stalled — while it is executing tools.
|
|
11
|
+
*/
|
|
12
|
+
export const ACTIVITY_WINDOW_MS = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* Classify agent-side activity. `unobservable` is NOT `quiet`: a backend that
|
|
15
|
+
* cannot see the agent (tmux) has no evidence, and no evidence must never be
|
|
16
|
+
* reported as "doing nothing".
|
|
17
|
+
*/
|
|
18
|
+
export function classifyActivity(activity, now = Date.now()) {
|
|
19
|
+
if (!activity)
|
|
20
|
+
return { state: 'unobservable' };
|
|
21
|
+
const lastUpdate = activity.lastUpdateAt ? Date.parse(activity.lastUpdateAt) : NaN;
|
|
22
|
+
const recent = Number.isFinite(lastUpdate) && now - lastUpdate <= ACTIVITY_WINDOW_MS;
|
|
23
|
+
return {
|
|
24
|
+
state: activity.activeToolCalls > 0 || recent ? 'active' : 'quiet',
|
|
25
|
+
activeToolCalls: activity.activeToolCalls,
|
|
26
|
+
...(activity.lastUpdateAt ? { lastUpdateAt: activity.lastUpdateAt } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One operator-facing line that never lets turn occupancy pose as liveness:
|
|
31
|
+
* the readiness value is labelled as the turn field it is, and the activity
|
|
32
|
+
* verdict is stated separately with the evidence behind it.
|
|
33
|
+
*/
|
|
34
|
+
export function describeSessionState(readiness, activity, now = Date.now()) {
|
|
35
|
+
const observed = classifyActivity(activity, now);
|
|
36
|
+
const evidence = [];
|
|
37
|
+
if (observed.activeToolCalls)
|
|
38
|
+
evidence.push(`${observed.activeToolCalls} tool calls in flight`);
|
|
39
|
+
if (observed.lastUpdateAt) {
|
|
40
|
+
const age = Math.max(0, Math.round((now - Date.parse(observed.lastUpdateAt)) / 1000));
|
|
41
|
+
if (Number.isFinite(age))
|
|
42
|
+
evidence.push(`last agent update ${age}s ago`);
|
|
43
|
+
}
|
|
44
|
+
const detail = observed.state === 'unobservable'
|
|
45
|
+
? 'no agent-side evidence on this backend'
|
|
46
|
+
: evidence.join(', ') || 'no updates yet';
|
|
47
|
+
return `turn: ${readiness ?? 'unknown'} (turn occupancy only) | activity: ${observed.state} (${detail})`;
|
|
48
|
+
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { SessionBackendId } from '../config.js';
|
|
2
2
|
import type { ConversationEventV1, ConversationSnapshot, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* TURN OCCUPANCY, and nothing else: `idle` means no fleet-tracked turn is in
|
|
5
|
+
* flight, which is exactly the question `arbiter.tryScheduled` asks before it
|
|
6
|
+
* admits a prompt. It is NOT a claim that the agent is doing nothing — a wake
|
|
7
|
+
* delivered through the `_session/steering` extension answers `startedNewTurn`
|
|
8
|
+
* and runs a whole turn that fleet never gets a `session/prompt` response for
|
|
9
|
+
* (ACP has no turn-end session update), so `readiness` stays `idle` for its
|
|
10
|
+
* entire duration. Anything reporting activity or liveness to a human must
|
|
11
|
+
* corroborate with `SessionSnapshot.activity` instead of reading `idle` here as
|
|
12
|
+
* "not working".
|
|
13
|
+
*/
|
|
3
14
|
export type SessionReadiness = 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed';
|
|
4
15
|
export type TurnOutcome = 'completed' | 'refused' | 'cancelled' | 'failed' | 'inconclusive';
|
|
5
16
|
export type TurnCancellationSource = 'owner' | 'local-console' | 'fleet-monitor' | 'scheduled-loop' | 'shutdown';
|
|
@@ -170,6 +181,19 @@ export interface SessionSnapshot {
|
|
|
170
181
|
/** Exact harness-native approval/permission mode used by this runner. */
|
|
171
182
|
nativeMode: string;
|
|
172
183
|
};
|
|
184
|
+
/**
|
|
185
|
+
* Observed agent activity, independent of turn occupancy: the evidence a
|
|
186
|
+
* human-facing surface needs before calling a role idle. Absent on backends
|
|
187
|
+
* that cannot observe the agent at all (tmux), which is itself honest — no
|
|
188
|
+
* evidence is not evidence of inactivity.
|
|
189
|
+
*/
|
|
190
|
+
activity?: SessionActivity;
|
|
191
|
+
}
|
|
192
|
+
export interface SessionActivity {
|
|
193
|
+
/** ACP tool calls currently reserved (lifecycle open or permission pending). */
|
|
194
|
+
activeToolCalls: number;
|
|
195
|
+
/** When the agent last sent ANY session update, replay excluded. */
|
|
196
|
+
lastUpdateAt?: string;
|
|
173
197
|
}
|
|
174
198
|
export type SessionEventKind = 'state' | 'agent_text' | 'thought' | 'tool_call' | 'tool_update' | 'permission' | 'monitor_delivery' | 'turn_stop' | 'error';
|
|
175
199
|
/** What a settled permission request resolved to. */
|
|
@@ -80,6 +80,13 @@ export function generateWatchdogBriefing(opts) {
|
|
|
80
80
|
L.push('- `healthy` — alive, on-briefing, recent progress.');
|
|
81
81
|
L.push('- `idle` — alive, nothing assigned or nothing to do. Not an anomaly.');
|
|
82
82
|
L.push('- `stale` = no worklog append and no console progress for ≥ 3 intervals.');
|
|
83
|
+
L.push('');
|
|
84
|
+
L.push('`session.readiness` from `ours-fleet status` is TURN OCCUPANCY, not activity: a mail');
|
|
85
|
+
L.push('wake delivered by ACP steering runs an entire turn while readiness stays `idle`. Never');
|
|
86
|
+
L.push('report `idle` or `stale` from `readiness=idle` alone — corroborate with the');
|
|
87
|
+
L.push('`activity:` line of the same `status` output (`active` means the agent is working),');
|
|
88
|
+
L.push('the worklog, or `ours-fleet peek`. `activity: unobservable` is missing evidence, not');
|
|
89
|
+
L.push('an idle agent.');
|
|
83
90
|
L.push('- `blocked` = waiting on a permission/prompt/modal longer than one interval.');
|
|
84
91
|
L.push('- `off_briefing` — activity contradicts the briefing (wrong repo, out-of-scope work,');
|
|
85
92
|
L.push(' ignored routine).');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.6",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|