@ours.network/fleet 0.12.0 → 0.13.0

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,6 +1,21 @@
1
1
  import type { SessionBackendId } from '../config.js';
2
2
  export type SessionReadiness = 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed';
3
3
  export type TurnOutcome = 'completed' | 'refused' | 'cancelled' | 'failed' | 'inconclusive';
4
+ export type TurnCancellationSource = 'owner' | 'local-console' | 'fleet-monitor' | 'scheduled-loop' | 'shutdown';
5
+ export type PromptOrigin = {
6
+ kind: 'startup';
7
+ } | {
8
+ kind: 'local-console';
9
+ } | {
10
+ kind: 'owner';
11
+ requestId: string;
12
+ } | {
13
+ kind: 'fleet-monitor';
14
+ } | {
15
+ kind: 'scheduled-loop';
16
+ loop: string;
17
+ runId: string;
18
+ };
4
19
  /**
5
20
  * Two independent facts about one turn, deliberately kept apart:
6
21
  *
@@ -19,6 +34,8 @@ export interface TurnResult {
19
34
  outcome: TurnOutcome;
20
35
  succeeded: boolean;
21
36
  detail?: string;
37
+ /** Present only when fleet can prove what initiated a cancelled turn. */
38
+ cancellationSource?: TurnCancellationSource;
22
39
  /** Final assistant text captured structurally by a backend, when available. */
23
40
  output?: string;
24
41
  }
@@ -41,6 +58,7 @@ export interface QueuedPrompt {
41
58
  promptId: string;
42
59
  /** Turns already queued ahead of this one. 0 means it starts immediately. */
43
60
  queuedBehind: number;
61
+ origin?: PromptOrigin;
44
62
  /** The turn's terminal result. Never rejects. */
45
63
  completion: Promise<TurnResult>;
46
64
  }
@@ -77,10 +95,14 @@ export declare function classifyChildExit(code: number | null, signal: string |
77
95
  /** The single definition of terminal success. Nothing else may re-derive it. */
78
96
  export declare const isTerminalSuccess: (outcome: TurnOutcome) => boolean;
79
97
  /** Build a TurnResult with `succeeded` always consistent with `outcome`. */
80
- export declare function turnResult(accepted: boolean, outcome: TurnOutcome, detail?: string, output?: string): TurnResult;
98
+ export declare function turnResult(accepted: boolean, outcome: TurnOutcome, detail?: string, output?: string, cancellationSource?: TurnCancellationSource): TurnResult;
81
99
  export interface SubmitPromptOptions {
82
100
  /** Cancel active work before delivering this prompt. */
83
101
  interrupt?: boolean;
102
+ /** Internal provenance; ordinary callers must leave this unset. */
103
+ interruptSource?: TurnCancellationSource;
104
+ /** Typed in-process provenance. Text markers never grant this origin. */
105
+ origin?: PromptOrigin;
84
106
  /** Use the ACP steering extension when available; ignored by other backends. */
85
107
  steer?: boolean;
86
108
  }
@@ -107,6 +129,8 @@ export interface SessionEvent {
107
129
  title?: string;
108
130
  status?: string;
109
131
  stopReason?: string;
132
+ origin?: PromptOrigin;
133
+ cancellationSource?: TurnCancellationSource;
110
134
  options?: Array<{
111
135
  optionId: string;
112
136
  name: string;
@@ -135,7 +159,7 @@ export interface SessionHandle {
135
159
  queuePrompt(text: string, options?: SubmitPromptOptions): Promise<QueuedPrompt>;
136
160
  /** Queue a prompt and wait for its terminal result. */
137
161
  submitPrompt(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
138
- interrupt(): Promise<void>;
162
+ interrupt(source?: TurnCancellationSource): Promise<void>;
139
163
  respondPermission(permissionId: string, optionId: string): boolean;
140
164
  eventsSince(seq: number): SessionEvent[];
141
165
  subscribe(listener: (event: SessionEvent) => void): () => void;
@@ -37,6 +37,9 @@ export function classifyChildExit(code, signal) {
37
37
  /** The single definition of terminal success. Nothing else may re-derive it. */
38
38
  export const isTerminalSuccess = (outcome) => outcome === 'completed';
39
39
  /** Build a TurnResult with `succeeded` always consistent with `outcome`. */
40
- export function turnResult(accepted, outcome, detail, output) {
41
- return { accepted, outcome, succeeded: isTerminalSuccess(outcome), detail, output };
40
+ export function turnResult(accepted, outcome, detail, output, cancellationSource) {
41
+ return {
42
+ accepted, outcome, succeeded: isTerminalSuccess(outcome), detail, output,
43
+ ...(outcome === 'cancelled' && cancellationSource ? { cancellationSource } : {}),
44
+ };
42
45
  }
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { delimiter, dirname, join } from 'node:path';
3
3
  import { userInfo } from 'node:os';
4
4
  import { home } from '../paths.js';
5
5
  import { realExec } from '../exec.js';
@@ -15,6 +15,8 @@ export const busHint = (stderr) => /user scope bus|XDG_RUNTIME_DIR/.test(stderr)
15
15
  `\n (if linger is already on: export XDG_RUNTIME_DIR=/run/user/$(id -u))`
16
16
  : '';
17
17
  export const unitFor = (name) => `ours-fleet-agent@${name}.service`;
18
+ /** Quote a systemd unit argument and escape its specifier marker. */
19
+ const unitArg = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
18
20
  /**
19
21
  * systemd's own ActiveState vocabulary, classified. `activating` covers
20
22
  * `auto-restart` — the unit is mid-restart, not stopped, so its context stands.
@@ -60,6 +62,13 @@ export function makeSystemdBackend(exec = realExec) {
60
62
  async init(binPath) {
61
63
  const msgs = [];
62
64
  const unitDir = join(home(), '.config', 'systemd', 'user');
65
+ // Lingering user units often start before a login shell imports its PATH.
66
+ // Pin the Node runtime and persist the install-time PATH so the runner and
67
+ // children such as `ours-mcp proxy` resolve the same tools after reboot.
68
+ const servicePath = [...new Set([
69
+ dirname(process.execPath),
70
+ ...(process.env.PATH ?? '').split(delimiter),
71
+ ].filter(Boolean))].join(delimiter);
63
72
  mkdirSync(unitDir, { recursive: true });
64
73
  writeFileSync(join(unitDir, UNIT_TEMPLATE), `[Unit]
65
74
  Description=ours-fleet agent %i
@@ -67,7 +76,8 @@ After=default.target
67
76
 
68
77
  [Service]
69
78
  Type=simple
70
- ExecStart=${binPath} _run %i
79
+ Environment="PATH=${servicePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"
80
+ ExecStart=${unitArg(process.execPath)} ${unitArg(binPath)} _run %i
71
81
  # The RUNNER owns the child-session restart loop, with a counted, backed-off
72
82
  # circuit breaker (3.2). systemd must only recover the runner PROCESS crashing —
73
83
  # Restart=always here would resume the uncounted two-second relaunch loop, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
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",