@superjackfruitlabs/salaam 0.0.1 → 0.0.2
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/CHANGELOG.md +15 -0
- package/README.md +25 -17
- package/dist/src/a2a-backend.js +225 -0
- package/dist/src/a2a-inbound.js +487 -0
- package/dist/src/a2a-peer.js +370 -0
- package/dist/src/a2a-protocol.js +250 -0
- package/dist/src/agent-profiles.js +289 -0
- package/dist/src/children.js +132 -26
- package/dist/src/claude-provider.js +2 -179
- package/dist/src/cli-help.js +107 -6
- package/dist/src/cli.js +477 -13
- package/dist/src/config.js +26 -3
- package/dist/src/credentials.js +7 -2
- package/dist/src/external-runtime.js +1 -0
- package/dist/src/external-work.js +583 -0
- package/dist/src/harness-backend.js +466 -0
- package/dist/src/harness-container.js +450 -0
- package/dist/src/harness-model-gateway.js +104 -0
- package/dist/src/harness-prepare.js +88 -0
- package/dist/src/harness-probe.js +111 -0
- package/dist/src/harness-protocol.js +325 -0
- package/dist/src/harness-readiness.js +50 -0
- package/dist/src/harness-rpc.js +206 -0
- package/dist/src/harness-tool-identity.js +87 -0
- package/dist/src/harness-wire-bridge.js +387 -0
- package/dist/src/host.js +6 -1
- package/dist/src/house-extension.js +147 -64
- package/dist/src/live-provider.js +301 -0
- package/dist/src/managed.js +33 -3
- package/dist/src/protocol.js +9 -5
- package/dist/src/provider-auth.js +300 -0
- package/dist/src/provider-config.js +65 -17
- package/dist/src/provider-model.js +78 -0
- package/dist/src/provider-payload.js +264 -0
- package/dist/src/provider-transport.js +36 -0
- package/dist/src/provider-usage.js +59 -0
- package/dist/src/repository-snapshots.js +219 -0
- package/dist/src/service.js +274 -9
- package/dist/src/store.js +44 -2
- package/dist/src/supervisor.js +246 -35
- package/dist/src/task-budget.js +17 -8
- package/dist/src/task-context.js +15 -0
- package/dist/src/task-driver.js +38 -14
- package/dist/src/task-policy.js +20 -1
- package/dist/src/task-verification.js +3 -0
- package/dist/src/task-work.js +31 -0
- package/dist/src/team-work.js +378 -0
- package/dist/src/tui-view.js +65 -10
- package/dist/src/tui.js +4 -2
- package/dist/src/usage.js +18 -0
- package/dist/src/worker-boundary.js +298 -0
- package/dist/src/worker-workspaces.js +274 -0
- package/dist/src/workspaces.js +6 -2
- package/harness-runtime/codex-pins.json +10 -0
- package/harness-runtime/package-lock.json +560 -0
- package/harness-runtime/package.json +9 -0
- package/npm-shrinkwrap.json +57 -2
- package/package.json +2 -1
- package/src/a2a-backend.ts +254 -0
- package/src/a2a-inbound.ts +585 -0
- package/src/a2a-peer.ts +441 -0
- package/src/a2a-protocol.ts +286 -0
- package/src/agent-profiles.ts +379 -0
- package/src/children.ts +168 -27
- package/src/claude-provider.ts +2 -189
- package/src/cli-help.ts +107 -6
- package/src/cli.ts +602 -12
- package/src/config.ts +34 -3
- package/src/credentials.ts +9 -2
- package/src/external-runtime.ts +54 -0
- package/src/external-work.ts +663 -0
- package/src/harness-backend.ts +557 -0
- package/src/harness-container.ts +543 -0
- package/src/harness-model-gateway.ts +138 -0
- package/src/harness-prepare.ts +113 -0
- package/src/harness-probe.ts +150 -0
- package/src/harness-protocol.ts +388 -0
- package/src/harness-readiness.ts +71 -0
- package/src/harness-rpc.ts +244 -0
- package/src/harness-tool-identity.ts +91 -0
- package/src/harness-wire-bridge.ts +418 -0
- package/src/host.ts +11 -6
- package/src/house-extension.ts +165 -66
- package/src/live-provider.ts +347 -0
- package/src/managed.ts +43 -4
- package/src/protocol.ts +36 -8
- package/src/provider-auth.ts +336 -0
- package/src/provider-config.ts +89 -18
- package/src/provider-model.ts +86 -0
- package/src/provider-payload.ts +263 -0
- package/src/provider-transport.ts +51 -0
- package/src/provider-usage.ts +64 -0
- package/src/repository-snapshots.ts +259 -0
- package/src/service.ts +446 -11
- package/src/store.ts +60 -4
- package/src/supervisor.ts +294 -41
- package/src/task-budget.ts +24 -11
- package/src/task-context.ts +17 -0
- package/src/task-driver.ts +51 -15
- package/src/task-policy.ts +23 -3
- package/src/task-verification.ts +2 -0
- package/src/task-work.ts +46 -1
- package/src/team-work.ts +554 -0
- package/src/tui-client.ts +5 -0
- package/src/tui-view.ts +164 -12
- package/src/tui.ts +5 -2
- package/src/usage.ts +17 -0
- package/src/worker-boundary.ts +395 -0
- package/src/worker-workspaces.ts +390 -0
- package/src/workspaces.ts +4 -2
package/src/store.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { chmodSync } from 'node:fs';
|
|
|
3
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { UsageLedger } from './usage.js';
|
|
6
|
+
import { WorkerBoundary } from './worker-boundary.js';
|
|
6
7
|
import { OFFLINE_ROUTE, type ProviderRoute } from './provider-config.js';
|
|
7
8
|
import { canonical } from './protocol.js';
|
|
8
9
|
import {
|
|
@@ -26,9 +27,11 @@ export class Store {
|
|
|
26
27
|
readonly db: DatabaseSync;
|
|
27
28
|
readonly changes = new EventEmitter();
|
|
28
29
|
readonly usage = new UsageLedger(this);
|
|
30
|
+
readonly workerBoundary = new WorkerBoundary(this);
|
|
29
31
|
reconcileCompactions?: (id: string) => void;
|
|
30
32
|
finishChild?: (attempt: Attempt) => void;
|
|
31
33
|
projectTaskAttempt?: (attempt: Attempt) => void;
|
|
34
|
+
teamSnapshot?: (session: string) => unknown;
|
|
32
35
|
guardTaskSubmission?: (session: string, step?: string) => void;
|
|
33
36
|
cancelChildren?: (attempt: string) => void;
|
|
34
37
|
recordTaskMutation?: (
|
|
@@ -59,7 +62,7 @@ export class Store {
|
|
|
59
62
|
);
|
|
60
63
|
const version = (this.db.prepare('PRAGMA user_version').get() as { user_version: number })
|
|
61
64
|
.user_version;
|
|
62
|
-
if (version >
|
|
65
|
+
if (version > 17) {
|
|
63
66
|
this.db.close();
|
|
64
67
|
throw new ServiceError('schema_too_new', 'Database schema is newer than this service');
|
|
65
68
|
}
|
|
@@ -202,6 +205,50 @@ export class Store {
|
|
|
202
205
|
CREATE TABLE recurring_occurrences(id TEXT PRIMARY KEY,job_id TEXT NOT NULL REFERENCES recurring_jobs(id),occurrence_key TEXT NOT NULL,session_id TEXT NOT NULL UNIQUE REFERENCES sessions(id),job_run_id TEXT UNIQUE REFERENCES jobs(id),schedule_revision INTEGER NOT NULL,due_at INTEGER,state TEXT NOT NULL,error TEXT,created_at INTEGER NOT NULL,UNIQUE(job_id,occurrence_key));
|
|
203
206
|
CREATE TABLE operator_inbox(id TEXT PRIMARY KEY,session_id TEXT NOT NULL REFERENCES sessions(id),source_key TEXT NOT NULL UNIQUE,kind TEXT NOT NULL,detail TEXT NOT NULL,revision INTEGER NOT NULL,state TEXT NOT NULL,ack_revision INTEGER NOT NULL DEFAULT 0,updated_at INTEGER NOT NULL);
|
|
204
207
|
PRAGMA user_version=11; COMMIT;`);
|
|
208
|
+
if (
|
|
209
|
+
(this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 12
|
|
210
|
+
) {
|
|
211
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
212
|
+
this.workerBoundary.migrate();
|
|
213
|
+
this.db.exec('PRAGMA user_version=12; COMMIT;');
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
(this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 13
|
|
217
|
+
)
|
|
218
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
219
|
+
CREATE TABLE agent_grants(id TEXT PRIMARY KEY,owner_session TEXT NOT NULL REFERENCES sessions(id),profile_id TEXT NOT NULL,document TEXT NOT NULL,digest TEXT NOT NULL,state TEXT NOT NULL);
|
|
220
|
+
CREATE TABLE specialist_reports(worker_id TEXT PRIMARY KEY,input_digest TEXT NOT NULL,artifact_id TEXT NOT NULL REFERENCES artifacts(id));
|
|
221
|
+
PRAGMA user_version=13; COMMIT;`);
|
|
222
|
+
if (
|
|
223
|
+
(this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 14
|
|
224
|
+
)
|
|
225
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
226
|
+
CREATE TABLE team_assignments(id TEXT PRIMARY KEY,task_id TEXT NOT NULL REFERENCES durable_tasks(id),step_id TEXT NOT NULL REFERENCES task_steps(id),plan_revision INTEGER NOT NULL,profile_id TEXT NOT NULL,grant_id TEXT NOT NULL REFERENCES agent_grants(id),budget INTEGER NOT NULL,generation INTEGER NOT NULL DEFAULT 0,child_id TEXT REFERENCES child_tasks(id),superseded INTEGER NOT NULL DEFAULT 0);
|
|
227
|
+
CREATE UNIQUE INDEX team_active_step ON team_assignments(step_id) WHERE superseded=0;
|
|
228
|
+
CREATE TABLE team_messages(id TEXT PRIMARY KEY,task_id TEXT NOT NULL REFERENCES durable_tasks(id),sender TEXT NOT NULL,recipient TEXT NOT NULL REFERENCES team_assignments(id),text TEXT NOT NULL,input_digest TEXT NOT NULL,created_at INTEGER NOT NULL,delivered_at INTEGER,acknowledged_at INTEGER);
|
|
229
|
+
PRAGMA user_version=14; COMMIT;`);
|
|
230
|
+
if (
|
|
231
|
+
(this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 15
|
|
232
|
+
)
|
|
233
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
234
|
+
CREATE TABLE worker_integrations(id TEXT PRIMARY KEY,parent_session TEXT NOT NULL REFERENCES sessions(id),child_id TEXT NOT NULL UNIQUE REFERENCES child_tasks(id),worker_id TEXT NOT NULL,patch_digest TEXT NOT NULL,before_revision INTEGER NOT NULL,before_digest TEXT NOT NULL,after_artifact TEXT NOT NULL REFERENCES artifacts(id),state TEXT NOT NULL,mutation_id TEXT REFERENCES workspace_mutations(id),mutation_key TEXT NOT NULL UNIQUE,generation INTEGER NOT NULL DEFAULT 1);
|
|
235
|
+
CREATE TABLE team_rejections(step_id TEXT NOT NULL REFERENCES task_steps(id),attempt_id TEXT NOT NULL REFERENCES attempts(id),request_id TEXT NOT NULL,reason TEXT NOT NULL,PRIMARY KEY(step_id,attempt_id));
|
|
236
|
+
PRAGMA user_version=15; COMMIT;`);
|
|
237
|
+
if (
|
|
238
|
+
(this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 16
|
|
239
|
+
)
|
|
240
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
241
|
+
CREATE TABLE external_assignments(assignment_id TEXT PRIMARY KEY REFERENCES team_assignments(id),runtime TEXT NOT NULL,digest TEXT NOT NULL);
|
|
242
|
+
CREATE TABLE external_runs(child_id TEXT PRIMARY KEY REFERENCES child_tasks(id),run_id TEXT NOT NULL UNIQUE,state TEXT NOT NULL,epoch INTEGER NOT NULL,checkpoint TEXT,receipt TEXT,stopped INTEGER NOT NULL DEFAULT 0,error TEXT,result TEXT,import_artifact TEXT REFERENCES artifacts(id));
|
|
243
|
+
CREATE TABLE task_invocations(attempt_id TEXT PRIMARY KEY REFERENCES attempts(id),task_id TEXT NOT NULL REFERENCES durable_tasks(id),runtime_id TEXT NOT NULL,input_digest TEXT NOT NULL,created_at INTEGER NOT NULL);
|
|
244
|
+
CREATE TABLE external_model_requests(id TEXT PRIMARY KEY,child_id TEXT NOT NULL REFERENCES external_runs(child_id),input_bytes INTEGER NOT NULL,output_bound INTEGER NOT NULL);
|
|
245
|
+
PRAGMA user_version=16; COMMIT;`);
|
|
246
|
+
if (
|
|
247
|
+
(this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 17
|
|
248
|
+
)
|
|
249
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
250
|
+
CREATE TABLE a2a_inbound(id TEXT PRIMARY KEY,peer TEXT NOT NULL,tenant TEXT NOT NULL,message_id TEXT NOT NULL,input_digest TEXT NOT NULL,assignment_id TEXT NOT NULL UNIQUE REFERENCES team_assignments(id),grant_digest TEXT NOT NULL,context_id TEXT NOT NULL UNIQUE,child_id TEXT NOT NULL UNIQUE REFERENCES child_tasks(id),created_at INTEGER NOT NULL,skill TEXT NOT NULL,cancel_requested INTEGER NOT NULL DEFAULT 0,UNIQUE(peer,tenant,message_id));
|
|
251
|
+
PRAGMA user_version=17; COMMIT;`);
|
|
205
252
|
} catch (error) {
|
|
206
253
|
if (this.db.isTransaction) this.db.exec('ROLLBACK');
|
|
207
254
|
this.db.close();
|
|
@@ -785,9 +832,18 @@ export class Store {
|
|
|
785
832
|
history_truncated: cursor !== null,
|
|
786
833
|
approvals: this.approvals(id),
|
|
787
834
|
operations: this.operations(id),
|
|
788
|
-
children:
|
|
789
|
-
.
|
|
790
|
-
|
|
835
|
+
children: (
|
|
836
|
+
this.db
|
|
837
|
+
.prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid DESC LIMIT 100')
|
|
838
|
+
.all(id) as unknown as SessionSnapshot['children']
|
|
839
|
+
).map((child) => ({
|
|
840
|
+
...child,
|
|
841
|
+
worker: this.workerBoundary.view(child.id),
|
|
842
|
+
})),
|
|
843
|
+
...(this.workerBoundary.profiles
|
|
844
|
+
? { agents: this.workerBoundary.profiles.snapshot(id) }
|
|
845
|
+
: {}),
|
|
846
|
+
...(this.teamSnapshot ? { team: this.teamSnapshot(id) } : {}),
|
|
791
847
|
};
|
|
792
848
|
}
|
|
793
849
|
cancelled(id: string): boolean {
|
package/src/supervisor.ts
CHANGED
|
@@ -25,13 +25,19 @@ import type { ChildWork } from './children.js';
|
|
|
25
25
|
import {
|
|
26
26
|
OFFLINE_ROUTE,
|
|
27
27
|
providerRoute,
|
|
28
|
-
|
|
28
|
+
providerHasOutputCap,
|
|
29
29
|
type ProviderRoute,
|
|
30
30
|
} from './provider-config.js';
|
|
31
31
|
import type { TaskContext, Compaction } from './task-context.js';
|
|
32
|
-
import {
|
|
32
|
+
import { validateApiKey } from './credentials.js';
|
|
33
|
+
import { resolveProviderAuth } from './provider-auth.js';
|
|
34
|
+
import { bindProviderConfiguration } from './provider-model.js';
|
|
35
|
+
import { validateProviderPayload } from './provider-payload.js';
|
|
33
36
|
import { digest } from './protocol.js';
|
|
34
37
|
import type { CodingWork } from './coding.js';
|
|
38
|
+
import type { TaskLimits } from './task-policy.js';
|
|
39
|
+
import { repositoryPath, type RepositorySnapshots } from './repository-snapshots.js';
|
|
40
|
+
import type { TeamWork } from './team-work.js';
|
|
35
41
|
|
|
36
42
|
export interface Launch {
|
|
37
43
|
command: string;
|
|
@@ -49,8 +55,12 @@ export interface SupervisorOptions {
|
|
|
49
55
|
effectsEnabled?: boolean;
|
|
50
56
|
providerRoute?: ProviderRoute;
|
|
51
57
|
anthropicApiKey?: string;
|
|
58
|
+
providerCredentials?: import('@earendil-works/pi-ai').CredentialStore;
|
|
59
|
+
providerEnvironment?: NodeJS.ProcessEnv;
|
|
52
60
|
}
|
|
53
61
|
interface Worker {
|
|
62
|
+
route: ProviderRoute;
|
|
63
|
+
providerOutputCap?: boolean;
|
|
54
64
|
id: string;
|
|
55
65
|
epoch: number;
|
|
56
66
|
token: string;
|
|
@@ -100,18 +110,79 @@ export class Supervisor extends EventEmitter {
|
|
|
100
110
|
managed?: ManagedWork;
|
|
101
111
|
children?: ChildWork;
|
|
102
112
|
coding?: CodingWork;
|
|
113
|
+
repositories?: RepositorySnapshots;
|
|
114
|
+
team?: TeamWork;
|
|
115
|
+
routeFor(id: string): ProviderRoute {
|
|
116
|
+
const child = this.children?.owned(id);
|
|
117
|
+
return child
|
|
118
|
+
? (this.store.workerBoundary.view(child.id).binding.agent?.route ?? this.route)
|
|
119
|
+
: this.route;
|
|
120
|
+
}
|
|
121
|
+
async prepareProviderRoute(input: ProviderRoute) {
|
|
122
|
+
const route = structuredClone(input);
|
|
123
|
+
if (route.provider === 'salaam-proof') return { route, output_cap: true, auth: undefined };
|
|
124
|
+
const auth =
|
|
125
|
+
route.provider === 'anthropic' && this.apiKey
|
|
126
|
+
? { auth: { apiKey: this.apiKey }, env: {} }
|
|
127
|
+
: await resolveProviderAuth(
|
|
128
|
+
route,
|
|
129
|
+
this.options.providerCredentials,
|
|
130
|
+
this.options.providerEnvironment,
|
|
131
|
+
);
|
|
132
|
+
const bound = bindProviderConfiguration(route, auth);
|
|
133
|
+
return { route: bound, output_cap: providerHasOutputCap(bound, auth), auth };
|
|
134
|
+
}
|
|
103
135
|
private codingBinding(id: string): CodingBinding | undefined {
|
|
104
|
-
|
|
136
|
+
const child = this.children?.owned(id),
|
|
137
|
+
coding = this.coding?.binding(id);
|
|
138
|
+
if (!child) return coding;
|
|
139
|
+
const binding = this.store.workerBoundary.view(child.id).binding;
|
|
140
|
+
return binding.isolation && coding
|
|
141
|
+
? {
|
|
142
|
+
...coding,
|
|
143
|
+
files: binding.tools.includes('salaam_files'),
|
|
144
|
+
commands: coding.commands && binding.tools.includes('salaam_command'),
|
|
145
|
+
}
|
|
146
|
+
: undefined;
|
|
105
147
|
}
|
|
106
148
|
private tools(id: string): string[] {
|
|
149
|
+
const child = this.children?.owned(id);
|
|
150
|
+
if (child) return JSON.parse(child.authority) as string[];
|
|
107
151
|
return workerTools(
|
|
108
152
|
!!this.options.effectsEnabled,
|
|
109
153
|
!!this.children?.owned(id),
|
|
110
154
|
this.codingBinding(id),
|
|
111
155
|
);
|
|
112
156
|
}
|
|
157
|
+
private childBoundary(id: string, epoch: number) {
|
|
158
|
+
const child = this.children?.owned(id);
|
|
159
|
+
return child ? this.store.workerBoundary.assert(child, epoch, this.routeFor(id)) : undefined;
|
|
160
|
+
}
|
|
113
161
|
readonly route: ProviderRoute;
|
|
114
162
|
private apiKey?: string;
|
|
163
|
+
private outputCap = true;
|
|
164
|
+
private authInitialized = false;
|
|
165
|
+
providerCapabilities() {
|
|
166
|
+
return {
|
|
167
|
+
output_token_cap: this.outputCap,
|
|
168
|
+
durable_tasks: true,
|
|
169
|
+
warning: this.outputCap
|
|
170
|
+
? null
|
|
171
|
+
: 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
assertTaskProviderSupported(limits: TaskLimits, outputCap = this.outputCap) {
|
|
175
|
+
if (!outputCap && (limits.token_units !== null || limits.output_token_bound !== null))
|
|
176
|
+
throw new ServiceError(
|
|
177
|
+
'provider_capability',
|
|
178
|
+
'This provider authentication has no output cap; set limits.token_units to null and omit output_token_bound',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
assertSessionProviderSupported(id: string, outputCap = this.outputCap) {
|
|
182
|
+
const parent = this.children?.owned(id)?.parent_session ?? id;
|
|
183
|
+
const task = this.tasks.work?.forSession(parent);
|
|
184
|
+
if (task) this.assertTaskProviderSupported(task.limits, outputCap);
|
|
185
|
+
}
|
|
115
186
|
readonly bootId = randomUUID();
|
|
116
187
|
private workers = new Map<string, Worker>();
|
|
117
188
|
private monitor: NodeJS.Timeout;
|
|
@@ -121,27 +192,54 @@ export class Supervisor extends EventEmitter {
|
|
|
121
192
|
readonly options: SupervisorOptions,
|
|
122
193
|
) {
|
|
123
194
|
super();
|
|
124
|
-
this.route = options.providerRoute ??
|
|
195
|
+
this.route = structuredClone(options.providerRoute ?? OFFLINE_ROUTE);
|
|
125
196
|
// Validate public configuration without trusting programmatic callers.
|
|
126
197
|
const checked = providerRoute(
|
|
127
198
|
this.route.provider === 'salaam-proof' ? 'offline' : this.route.provider,
|
|
128
|
-
this.route.provider
|
|
129
|
-
this.route.provider
|
|
199
|
+
this.route.provider !== 'salaam-proof' ? this.route.model : undefined,
|
|
200
|
+
this.route.provider !== 'salaam-proof' ? this.route.max_tokens : undefined,
|
|
130
201
|
this.route.workspace_id,
|
|
202
|
+
this.route.connection,
|
|
131
203
|
);
|
|
132
204
|
if (canonical(checked) !== canonical(this.route)) throw new Error('Invalid provider route');
|
|
133
|
-
if (this.
|
|
134
|
-
this.apiKey = options.anthropicApiKey
|
|
135
|
-
? validateApiKey(options.anthropicApiKey)
|
|
136
|
-
: loadApiKey();
|
|
205
|
+
if (options.anthropicApiKey) this.apiKey = validateApiKey(options.anthropicApiKey);
|
|
137
206
|
this.bundle = bundleDigest();
|
|
138
207
|
this.monitor = setInterval(() => {
|
|
139
208
|
for (const worker of this.workers.values())
|
|
140
|
-
if (worker.ready
|
|
141
|
-
|
|
209
|
+
if (worker.ready) {
|
|
210
|
+
if (Date.now() - worker.lastBeat > (options.heartbeatTimeout ?? 2500))
|
|
211
|
+
this.invalidate(worker, 'Worker heartbeat expired');
|
|
212
|
+
const child = this.children?.owned(worker.id);
|
|
213
|
+
if (child?.state === 'accepted') {
|
|
214
|
+
try {
|
|
215
|
+
this.childBoundary(worker.id, worker.epoch);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.invalidate(worker, 'Worker boundary revoked: ' + (error as Error).message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
142
221
|
}, 250);
|
|
143
222
|
this.monitor.unref();
|
|
144
223
|
}
|
|
224
|
+
async providerAuth() {
|
|
225
|
+
if (this.route.provider === 'salaam-proof') return undefined;
|
|
226
|
+
if (this.route.provider === 'anthropic' && this.apiKey)
|
|
227
|
+
return { auth: { apiKey: this.apiKey }, env: {} };
|
|
228
|
+
const auth = await resolveProviderAuth(
|
|
229
|
+
this.route,
|
|
230
|
+
this.options.providerCredentials,
|
|
231
|
+
this.options.providerEnvironment,
|
|
232
|
+
);
|
|
233
|
+
Object.assign(this.route, bindProviderConfiguration(this.route, auth));
|
|
234
|
+
const outputCap = providerHasOutputCap(this.route, auth);
|
|
235
|
+
if (this.authInitialized && this.outputCap !== outputCap)
|
|
236
|
+
throw new Error(
|
|
237
|
+
'Provider output-cap capability changed; restart the service before using the new authentication',
|
|
238
|
+
);
|
|
239
|
+
this.outputCap = outputCap;
|
|
240
|
+
this.authInitialized = true;
|
|
241
|
+
return auth;
|
|
242
|
+
}
|
|
145
243
|
async recover(): Promise<void> {
|
|
146
244
|
const rows = this.store.db.prepare("SELECT * FROM workers WHERE state!='exited'").all() as {
|
|
147
245
|
session_id: string;
|
|
@@ -177,6 +275,13 @@ export class Supervisor extends EventEmitter {
|
|
|
177
275
|
}
|
|
178
276
|
}
|
|
179
277
|
async start(id: string): Promise<void> {
|
|
278
|
+
const child = this.children?.owned(id);
|
|
279
|
+
if (child && this.store.workerBoundary.view(child.id).binding.runtime !== 'native-pi')
|
|
280
|
+
throw new ServiceError(
|
|
281
|
+
'external_worker_owned',
|
|
282
|
+
'Use the external worker lifecycle; Pi cannot claim this session',
|
|
283
|
+
);
|
|
284
|
+
const route = this.routeFor(id);
|
|
180
285
|
const prior = this.workers.get(id);
|
|
181
286
|
if (prior && !prior.stopping)
|
|
182
287
|
throw new ServiceError('worker_exists', 'Worker already supervised');
|
|
@@ -193,7 +298,7 @@ export class Supervisor extends EventEmitter {
|
|
|
193
298
|
'reconciliation_required',
|
|
194
299
|
previous.error ?? 'Session requires reconciliation',
|
|
195
300
|
);
|
|
196
|
-
this.store.bindRoute(id,
|
|
301
|
+
this.store.bindRoute(id, route);
|
|
197
302
|
const epoch = this.store.allocate(id, this.bootId);
|
|
198
303
|
let accept!: () => void;
|
|
199
304
|
let reject!: (e: Error) => void;
|
|
@@ -204,6 +309,7 @@ export class Supervisor extends EventEmitter {
|
|
|
204
309
|
const binding = randomBytes(16).toString('hex');
|
|
205
310
|
const frame = this.tasks.frame(id, binding);
|
|
206
311
|
const worker: Worker = {
|
|
312
|
+
route: structuredClone(route),
|
|
207
313
|
id,
|
|
208
314
|
epoch,
|
|
209
315
|
token: randomBytes(32).toString('hex'),
|
|
@@ -224,6 +330,8 @@ export class Supervisor extends EventEmitter {
|
|
|
224
330
|
void handshake.catch(() => {});
|
|
225
331
|
let timer: NodeJS.Timeout | undefined;
|
|
226
332
|
try {
|
|
333
|
+
const child = this.children?.owned(id);
|
|
334
|
+
if (child) this.store.workerBoundary.claim(child, epoch, route);
|
|
227
335
|
const workerDir = join(this.options.stateDir, 'workers', id);
|
|
228
336
|
mkdirSync(join(workerDir, 'agent'), { recursive: true, mode: 0o700 });
|
|
229
337
|
writeFileSync(
|
|
@@ -245,7 +353,8 @@ export class Supervisor extends EventEmitter {
|
|
|
245
353
|
frame,
|
|
246
354
|
effects: !!this.options.effectsEnabled && !this.children?.owned(id),
|
|
247
355
|
child: !!this.children?.owned(id),
|
|
248
|
-
|
|
356
|
+
worker_binding: this.childBoundary(id, epoch),
|
|
357
|
+
route: route,
|
|
249
358
|
coding: this.codingBinding(id),
|
|
250
359
|
};
|
|
251
360
|
const args = [
|
|
@@ -271,9 +380,9 @@ export class Supervisor extends EventEmitter {
|
|
|
271
380
|
'--tools',
|
|
272
381
|
this.tools(id).join(','),
|
|
273
382
|
'--provider',
|
|
274
|
-
|
|
383
|
+
route.provider,
|
|
275
384
|
'--model',
|
|
276
|
-
|
|
385
|
+
route.model,
|
|
277
386
|
];
|
|
278
387
|
if (previous.pi_session_file && existsSync(previous.pi_session_file)) {
|
|
279
388
|
const index = args.indexOf('--session-id');
|
|
@@ -293,7 +402,7 @@ export class Supervisor extends EventEmitter {
|
|
|
293
402
|
LANG: process.env.LANG ?? 'en_US.UTF-8',
|
|
294
403
|
PI_CODING_AGENT_DIR: join(workerDir, 'agent'),
|
|
295
404
|
SALAAM_WORKER_CONFIG: JSON.stringify(config),
|
|
296
|
-
...(
|
|
405
|
+
...(route.connection?.configuration ?? {}),
|
|
297
406
|
};
|
|
298
407
|
worker.pi = (this.options.launch ?? ((x) => new PiProcess(x.command, x.args, x.cwd, x.env)))({
|
|
299
408
|
command: process.execPath,
|
|
@@ -387,16 +496,21 @@ export class Supervisor extends EventEmitter {
|
|
|
387
496
|
const file = typeof state.sessionFile === 'string' ? resolve(state.sessionFile) : null;
|
|
388
497
|
if (!file || !file.startsWith(resolve(sessionDir) + '/'))
|
|
389
498
|
throw new Error('Pi session is outside its managed directory');
|
|
390
|
-
if (
|
|
391
|
-
object(state.model).provider !== this.route.provider ||
|
|
392
|
-
object(state.model).id !== this.route.model
|
|
393
|
-
)
|
|
499
|
+
if (object(state.model).provider !== route.provider || object(state.model).id !== route.model)
|
|
394
500
|
throw new Error('Pi selected an unapproved provider route');
|
|
395
501
|
if (worker.invalid) throw new Error(worker.invalid);
|
|
396
502
|
// Pi buffers setup-only entries until there is a conversation. Do not
|
|
397
503
|
// persist a cursor into that ephemeral tree before its file exists.
|
|
398
504
|
if (existsSync(file)) await this.syncEntries(worker);
|
|
399
|
-
this.store.
|
|
505
|
+
if (child) this.store.workerBoundary.accepted(child, epoch, route);
|
|
506
|
+
this.store.state(
|
|
507
|
+
id,
|
|
508
|
+
epoch,
|
|
509
|
+
'ready',
|
|
510
|
+
null,
|
|
511
|
+
workerCapabilities(this.codingBinding(id), this.tools(id)),
|
|
512
|
+
file,
|
|
513
|
+
);
|
|
400
514
|
worker.ready = true;
|
|
401
515
|
worker.lastBeat = Date.now();
|
|
402
516
|
} catch (error) {
|
|
@@ -455,6 +569,7 @@ export class Supervisor extends EventEmitter {
|
|
|
455
569
|
body: Record<string, unknown>,
|
|
456
570
|
): Promise<unknown> {
|
|
457
571
|
const w = this.authenticate(id, token, body);
|
|
572
|
+
const route = w.route;
|
|
458
573
|
if (method === 'failure') {
|
|
459
574
|
if (!w.stopping) this.invalidate(w, 'Required extension failed');
|
|
460
575
|
return { ok: true };
|
|
@@ -464,6 +579,7 @@ export class Supervisor extends EventEmitter {
|
|
|
464
579
|
if (method === 'handshake') {
|
|
465
580
|
try {
|
|
466
581
|
if (w.manifest) throw new Error('Duplicate handshake');
|
|
582
|
+
const boundary = this.childBoundary(id, w.epoch);
|
|
467
583
|
const expected = {
|
|
468
584
|
protocol_version: PROTOCOL_VERSION,
|
|
469
585
|
bundle_digest: w.bundle,
|
|
@@ -471,13 +587,14 @@ export class Supervisor extends EventEmitter {
|
|
|
471
587
|
session_id: id,
|
|
472
588
|
pi_session_id: id,
|
|
473
589
|
epoch: w.epoch,
|
|
474
|
-
capabilities: workerCapabilities(this.codingBinding(id)),
|
|
590
|
+
capabilities: workerCapabilities(this.codingBinding(id), this.tools(id)),
|
|
475
591
|
tools: this.tools(id),
|
|
476
592
|
active_tools: this.tools(id),
|
|
477
593
|
...(this.codingBinding(id) ? { coding: this.codingBinding(id) } : {}),
|
|
478
|
-
provider:
|
|
479
|
-
route:
|
|
594
|
+
provider: route.provider,
|
|
595
|
+
route: route,
|
|
480
596
|
context_digest: textDigest(w.frame),
|
|
597
|
+
...(boundary ? { worker_binding: boundary } : {}),
|
|
481
598
|
};
|
|
482
599
|
for (const [key, value] of Object.entries(expected))
|
|
483
600
|
if (canonical(body[key]) !== canonical(value))
|
|
@@ -499,12 +616,98 @@ export class Supervisor extends EventEmitter {
|
|
|
499
616
|
if (w.attempt && this.store.cancelled(w.attempt))
|
|
500
617
|
throw new ServiceError('attempt_cancelled', 'Attempt cancellation revoked further grants');
|
|
501
618
|
try {
|
|
619
|
+
this.childBoundary(id, w.epoch);
|
|
620
|
+
if (method.startsWith('team.')) {
|
|
621
|
+
const child = this.children?.owned(id);
|
|
622
|
+
if (!child || !this.team || !w.attempt) throw new Error('No owned team assignment');
|
|
623
|
+
const binding = this.store.workerBoundary.view(child.id).binding;
|
|
624
|
+
if (!binding.assignment || !binding.tools.includes('salaam_team'))
|
|
625
|
+
throw new Error('No team message authority');
|
|
626
|
+
const authorize = () => {
|
|
627
|
+
this.assertActive(id, w.epoch, w.attempt!);
|
|
628
|
+
this.team!.assert(binding);
|
|
629
|
+
this.store.taskGrantCheck?.(id);
|
|
630
|
+
};
|
|
631
|
+
authorize();
|
|
632
|
+
const request = string(body.request_id, 'request_id', 128),
|
|
633
|
+
assignment = binding.assignment.id;
|
|
634
|
+
if (method === 'team.send')
|
|
635
|
+
return this.team.send(
|
|
636
|
+
binding.owner_task!,
|
|
637
|
+
assignment,
|
|
638
|
+
string(body.recipient, 'recipient', 128),
|
|
639
|
+
string(body.text, 'text', 4096),
|
|
640
|
+
request,
|
|
641
|
+
authorize,
|
|
642
|
+
);
|
|
643
|
+
if (method === 'team.receive') return this.team.receive(assignment, request, authorize);
|
|
644
|
+
if (method === 'team.ack')
|
|
645
|
+
return this.team.acknowledge(
|
|
646
|
+
assignment,
|
|
647
|
+
string(body.message_id, 'message_id', 128),
|
|
648
|
+
request,
|
|
649
|
+
authorize,
|
|
650
|
+
);
|
|
651
|
+
throw new ServiceError('invalid_field', 'Unknown team method', 400);
|
|
652
|
+
}
|
|
653
|
+
if (method === 'specialist.read' || method === 'specialist.report') {
|
|
654
|
+
const child = this.children?.owned(id);
|
|
655
|
+
if (!child || !this.repositories || !w.attempt)
|
|
656
|
+
throw new Error('No active specialist assignment');
|
|
657
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
658
|
+
const binding = this.store.workerBoundary.view(child.id).binding;
|
|
659
|
+
const tool = method === 'specialist.read' ? 'salaam_repository' : 'salaam_findings';
|
|
660
|
+
if (!binding.tools.includes(tool)) throw new Error('Specialist authority is unavailable');
|
|
661
|
+
this.store.taskGrantCheck?.(id);
|
|
662
|
+
if (method === 'specialist.report') return this.repositories.report(binding, body.findings);
|
|
663
|
+
const args = object(body.args);
|
|
664
|
+
return this.store.request(
|
|
665
|
+
`specialist-read:${id}:${w.epoch}`,
|
|
666
|
+
string(body.tool_call_id, 'tool_call_id', 512),
|
|
667
|
+
args,
|
|
668
|
+
() => {
|
|
669
|
+
const result = this.repositories!.read(binding, args);
|
|
670
|
+
this.store.event(id, 'specialist.read', {
|
|
671
|
+
attempt_id: w.attempt,
|
|
672
|
+
snapshot_digest: binding.repository?.digest,
|
|
673
|
+
argument_digest: digest(args),
|
|
674
|
+
result_digest: digest(result),
|
|
675
|
+
});
|
|
676
|
+
return result;
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (method === 'provider.auth') {
|
|
681
|
+
if (!this.children?.owned(id)) {
|
|
682
|
+
const auth = await this.providerAuth();
|
|
683
|
+
if (canonical(this.route) !== canonical(route)) throw new Error('Provider route changed');
|
|
684
|
+
w.providerOutputCap = this.outputCap;
|
|
685
|
+
this.assertSessionProviderSupported(id, this.outputCap);
|
|
686
|
+
return auth;
|
|
687
|
+
}
|
|
688
|
+
const prepared = await this.prepareProviderRoute(route);
|
|
689
|
+
const expectedCap = this.children?.owned(id)
|
|
690
|
+
? this.store.workerBoundary.view(this.children.owned(id)!.id).binding.agent?.output_cap
|
|
691
|
+
: this.outputCap;
|
|
692
|
+
if (
|
|
693
|
+
canonical(prepared.route) !== canonical(route) ||
|
|
694
|
+
(expectedCap ?? this.outputCap) !== prepared.output_cap
|
|
695
|
+
)
|
|
696
|
+
throw new Error('Provider capability or route changed; restart or renew admission');
|
|
697
|
+
w.providerOutputCap = prepared.output_cap;
|
|
698
|
+
this.assertSessionProviderSupported(id, prepared.output_cap);
|
|
699
|
+
const auth = prepared.auth;
|
|
700
|
+
return auth;
|
|
701
|
+
}
|
|
502
702
|
if (method === 'coding.files' || method === 'coding.command') {
|
|
503
|
-
if (
|
|
703
|
+
if (!this.coding || !this.codingBinding(id) || !w.attempt)
|
|
504
704
|
throw new Error('Coding authority is unavailable');
|
|
505
705
|
this.assertActive(id, w.epoch, w.attempt);
|
|
706
|
+
this.store.taskGrantCheck?.(id);
|
|
506
707
|
const args = object(body.args);
|
|
507
|
-
if (method === 'coding.files')
|
|
708
|
+
if (method === 'coding.files') {
|
|
709
|
+
if (!this.codingBinding(id)?.files) throw new Error('File authority is unavailable');
|
|
710
|
+
if (this.children?.owned(id) && args.path !== undefined) repositoryPath(args.path);
|
|
508
711
|
return this.coding.file(
|
|
509
712
|
id,
|
|
510
713
|
w.epoch,
|
|
@@ -512,6 +715,7 @@ export class Supervisor extends EventEmitter {
|
|
|
512
715
|
string(body.tool_call_id, 'tool_call_id', 512),
|
|
513
716
|
args,
|
|
514
717
|
);
|
|
718
|
+
}
|
|
515
719
|
if (!this.codingBinding(id)?.commands) throw new Error('Command execution is unavailable');
|
|
516
720
|
if (args.action === 'start')
|
|
517
721
|
return this.coding.start(id, w.attempt, args, () =>
|
|
@@ -557,8 +761,16 @@ export class Supervisor extends EventEmitter {
|
|
|
557
761
|
}
|
|
558
762
|
if (method.startsWith('child.')) {
|
|
559
763
|
if (!this.children || !w.attempt) throw new Error('No active parent attempt');
|
|
560
|
-
|
|
561
|
-
if (method
|
|
764
|
+
// spawn() resolves a saved step before testing a fresh invocation/grant.
|
|
765
|
+
if (method !== 'child.spawn') this.assertActive(id, w.epoch, w.attempt);
|
|
766
|
+
if (method === 'child.spawn') {
|
|
767
|
+
if (
|
|
768
|
+
Object.keys(body).some(
|
|
769
|
+
(key) =>
|
|
770
|
+
!['epoch', 'step_key', 'prompt', 'budget', 'authority', 'profile'].includes(key),
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
throw new ServiceError('invalid_child', 'Unsupported child admission field');
|
|
562
774
|
return this.children.spawn(
|
|
563
775
|
id,
|
|
564
776
|
w.epoch,
|
|
@@ -568,8 +780,12 @@ export class Supervisor extends EventEmitter {
|
|
|
568
780
|
Number(body.budget),
|
|
569
781
|
Array.isArray(body.authority)
|
|
570
782
|
? body.authority.map((x) => string(x, 'authority', 128))
|
|
571
|
-
:
|
|
783
|
+
: body.profile
|
|
784
|
+
? []
|
|
785
|
+
: ['salaam_probe'],
|
|
786
|
+
body.profile === undefined ? undefined : string(body.profile, 'profile', 128),
|
|
572
787
|
);
|
|
788
|
+
}
|
|
573
789
|
if (method === 'child.list') return this.children.list(id);
|
|
574
790
|
const child = string(body.child_id, 'child_id', 128);
|
|
575
791
|
if (method === 'child.inspect') return this.children.inspect(id, child);
|
|
@@ -614,10 +830,10 @@ export class Supervisor extends EventEmitter {
|
|
|
614
830
|
this.store.usage.observe(
|
|
615
831
|
id,
|
|
616
832
|
string(body.source_key, 'source_key', 256),
|
|
617
|
-
|
|
618
|
-
|
|
833
|
+
route.provider,
|
|
834
|
+
route.model,
|
|
619
835
|
body.usage,
|
|
620
|
-
|
|
836
|
+
route.provider !== 'salaam-proof' ? 'estimated' : 'known',
|
|
621
837
|
),
|
|
622
838
|
);
|
|
623
839
|
return { ok: true };
|
|
@@ -626,22 +842,26 @@ export class Supervisor extends EventEmitter {
|
|
|
626
842
|
this.coding?.assertSafe(id);
|
|
627
843
|
this.store.taskGrantCheck?.(id);
|
|
628
844
|
if (
|
|
629
|
-
body.provider !==
|
|
630
|
-
body.model !==
|
|
845
|
+
body.provider !== route.provider ||
|
|
846
|
+
body.model !== route.model ||
|
|
631
847
|
body.context_digest !== textDigest(this.tasks.frame(id, w.binding))
|
|
632
848
|
)
|
|
633
849
|
throw new Error('Provider or task context is invalid');
|
|
634
|
-
if (
|
|
635
|
-
|
|
850
|
+
if (route.provider !== 'salaam-proof') {
|
|
851
|
+
if (w.providerOutputCap === undefined)
|
|
852
|
+
throw new Error('Provider authentication was not admitted');
|
|
853
|
+
this.assertSessionProviderSupported(id, w.providerOutputCap);
|
|
854
|
+
validateProviderPayload(
|
|
636
855
|
body.payload,
|
|
637
|
-
|
|
856
|
+
route,
|
|
638
857
|
this.tasks.frame(id, w.binding),
|
|
639
858
|
this.tools(id),
|
|
859
|
+
!w.providerOutputCap,
|
|
640
860
|
);
|
|
641
861
|
if (body.payload_digest !== digest(body.payload))
|
|
642
|
-
throw new Error('
|
|
862
|
+
throw new Error('Provider payload digest changed');
|
|
643
863
|
}
|
|
644
|
-
if (
|
|
864
|
+
if (route.provider === 'salaam-proof') {
|
|
645
865
|
const payload = object(body.payload);
|
|
646
866
|
if (!Array.isArray(payload.messages)) throw new Error('Proof payload missing');
|
|
647
867
|
const last = object(payload.messages.at(-1));
|
|
@@ -666,7 +886,7 @@ export class Supervisor extends EventEmitter {
|
|
|
666
886
|
string(body.source_key, 'source_key', 256),
|
|
667
887
|
String(body.provider),
|
|
668
888
|
String(body.model),
|
|
669
|
-
{ input: Buffer.byteLength(JSON.stringify(body.payload)), output:
|
|
889
|
+
{ input: Buffer.byteLength(JSON.stringify(body.payload)), output: route.max_tokens },
|
|
670
890
|
);
|
|
671
891
|
this.store.transaction(() => {
|
|
672
892
|
this.store.event(id, 'model.admitted', {
|
|
@@ -692,6 +912,19 @@ export class Supervisor extends EventEmitter {
|
|
|
692
912
|
return { ok: true };
|
|
693
913
|
} catch (error) {
|
|
694
914
|
this.store.taskBudgetDenied?.(id, error);
|
|
915
|
+
if (
|
|
916
|
+
method.startsWith('specialist.') &&
|
|
917
|
+
error instanceof ServiceError &&
|
|
918
|
+
[
|
|
919
|
+
'not_found',
|
|
920
|
+
'binary_file',
|
|
921
|
+
'line_limit',
|
|
922
|
+
'invalid_field',
|
|
923
|
+
'unsafe_path',
|
|
924
|
+
'request_conflict',
|
|
925
|
+
].includes(error.code)
|
|
926
|
+
)
|
|
927
|
+
throw error;
|
|
695
928
|
if (
|
|
696
929
|
method.startsWith('coding.') &&
|
|
697
930
|
error instanceof ServiceError &&
|
|
@@ -740,9 +973,26 @@ export class Supervisor extends EventEmitter {
|
|
|
740
973
|
}
|
|
741
974
|
}
|
|
742
975
|
inspect(id: string): Session {
|
|
976
|
+
const externalChild = this.children?.owned(id);
|
|
977
|
+
if (externalChild && this.store.workerBoundary.view(externalChild.id).binding.external)
|
|
978
|
+
return this.store.session(id);
|
|
979
|
+
const child = this.children?.owned(id);
|
|
980
|
+
const cap = child
|
|
981
|
+
? this.store.workerBoundary.view(child.id).binding.agent?.output_cap
|
|
982
|
+
: undefined;
|
|
743
983
|
const state = {
|
|
744
984
|
...this.store.session(id),
|
|
745
985
|
provider_route: this.store.providerRoute(id) ?? undefined,
|
|
986
|
+
provider_capabilities:
|
|
987
|
+
cap === undefined
|
|
988
|
+
? this.providerCapabilities()
|
|
989
|
+
: {
|
|
990
|
+
output_token_cap: cap,
|
|
991
|
+
durable_tasks: true,
|
|
992
|
+
warning: cap
|
|
993
|
+
? null
|
|
994
|
+
: 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
|
|
995
|
+
},
|
|
746
996
|
};
|
|
747
997
|
const w = this.workers.get(id);
|
|
748
998
|
if (w?.invalid) return { ...state, state: 'unhealthy', capabilities: [], error: w.invalid };
|
|
@@ -942,6 +1192,7 @@ export class Supervisor extends EventEmitter {
|
|
|
942
1192
|
if (w.invalid || end.aborted) throw new Error(w.invalid ?? 'Attempt aborted');
|
|
943
1193
|
const entries = await this.readEntries(w);
|
|
944
1194
|
const result = object((await w.pi.command('get_last_assistant_text')).data);
|
|
1195
|
+
this.childBoundary(id, w.epoch);
|
|
945
1196
|
this.store.finish(
|
|
946
1197
|
work.attempt.id,
|
|
947
1198
|
outcomeError ? 'failed' : 'completed',
|
|
@@ -957,6 +1208,8 @@ export class Supervisor extends EventEmitter {
|
|
|
957
1208
|
w.pi.off('record', record);
|
|
958
1209
|
w.busy = false;
|
|
959
1210
|
delete w.attempt;
|
|
1211
|
+
const child = this.children?.owned(id);
|
|
1212
|
+
if (child && child.state !== 'accepted') await this.stopWorker(w);
|
|
960
1213
|
this.emit('idle', id);
|
|
961
1214
|
}
|
|
962
1215
|
return true;
|