@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +25 -17
  3. package/dist/src/a2a-backend.js +225 -0
  4. package/dist/src/a2a-inbound.js +487 -0
  5. package/dist/src/a2a-peer.js +370 -0
  6. package/dist/src/a2a-protocol.js +250 -0
  7. package/dist/src/agent-profiles.js +289 -0
  8. package/dist/src/children.js +132 -26
  9. package/dist/src/claude-provider.js +2 -179
  10. package/dist/src/cli-help.js +107 -6
  11. package/dist/src/cli.js +477 -13
  12. package/dist/src/config.js +26 -3
  13. package/dist/src/credentials.js +7 -2
  14. package/dist/src/external-runtime.js +1 -0
  15. package/dist/src/external-work.js +583 -0
  16. package/dist/src/harness-backend.js +466 -0
  17. package/dist/src/harness-container.js +450 -0
  18. package/dist/src/harness-model-gateway.js +104 -0
  19. package/dist/src/harness-prepare.js +88 -0
  20. package/dist/src/harness-probe.js +111 -0
  21. package/dist/src/harness-protocol.js +325 -0
  22. package/dist/src/harness-readiness.js +50 -0
  23. package/dist/src/harness-rpc.js +206 -0
  24. package/dist/src/harness-tool-identity.js +87 -0
  25. package/dist/src/harness-wire-bridge.js +387 -0
  26. package/dist/src/host.js +6 -1
  27. package/dist/src/house-extension.js +147 -64
  28. package/dist/src/live-provider.js +301 -0
  29. package/dist/src/managed.js +33 -3
  30. package/dist/src/protocol.js +9 -5
  31. package/dist/src/provider-auth.js +300 -0
  32. package/dist/src/provider-config.js +65 -17
  33. package/dist/src/provider-model.js +78 -0
  34. package/dist/src/provider-payload.js +264 -0
  35. package/dist/src/provider-transport.js +36 -0
  36. package/dist/src/provider-usage.js +59 -0
  37. package/dist/src/repository-snapshots.js +219 -0
  38. package/dist/src/service.js +274 -9
  39. package/dist/src/store.js +44 -2
  40. package/dist/src/supervisor.js +246 -35
  41. package/dist/src/task-budget.js +17 -8
  42. package/dist/src/task-context.js +15 -0
  43. package/dist/src/task-driver.js +38 -14
  44. package/dist/src/task-policy.js +20 -1
  45. package/dist/src/task-verification.js +3 -0
  46. package/dist/src/task-work.js +31 -0
  47. package/dist/src/team-work.js +378 -0
  48. package/dist/src/tui-view.js +65 -10
  49. package/dist/src/tui.js +4 -2
  50. package/dist/src/usage.js +18 -0
  51. package/dist/src/worker-boundary.js +298 -0
  52. package/dist/src/worker-workspaces.js +274 -0
  53. package/dist/src/workspaces.js +6 -2
  54. package/harness-runtime/codex-pins.json +10 -0
  55. package/harness-runtime/package-lock.json +560 -0
  56. package/harness-runtime/package.json +9 -0
  57. package/npm-shrinkwrap.json +57 -2
  58. package/package.json +2 -1
  59. package/src/a2a-backend.ts +254 -0
  60. package/src/a2a-inbound.ts +585 -0
  61. package/src/a2a-peer.ts +441 -0
  62. package/src/a2a-protocol.ts +286 -0
  63. package/src/agent-profiles.ts +379 -0
  64. package/src/children.ts +168 -27
  65. package/src/claude-provider.ts +2 -189
  66. package/src/cli-help.ts +107 -6
  67. package/src/cli.ts +602 -12
  68. package/src/config.ts +34 -3
  69. package/src/credentials.ts +9 -2
  70. package/src/external-runtime.ts +54 -0
  71. package/src/external-work.ts +663 -0
  72. package/src/harness-backend.ts +557 -0
  73. package/src/harness-container.ts +543 -0
  74. package/src/harness-model-gateway.ts +138 -0
  75. package/src/harness-prepare.ts +113 -0
  76. package/src/harness-probe.ts +150 -0
  77. package/src/harness-protocol.ts +388 -0
  78. package/src/harness-readiness.ts +71 -0
  79. package/src/harness-rpc.ts +244 -0
  80. package/src/harness-tool-identity.ts +91 -0
  81. package/src/harness-wire-bridge.ts +418 -0
  82. package/src/host.ts +11 -6
  83. package/src/house-extension.ts +165 -66
  84. package/src/live-provider.ts +347 -0
  85. package/src/managed.ts +43 -4
  86. package/src/protocol.ts +36 -8
  87. package/src/provider-auth.ts +336 -0
  88. package/src/provider-config.ts +89 -18
  89. package/src/provider-model.ts +86 -0
  90. package/src/provider-payload.ts +263 -0
  91. package/src/provider-transport.ts +51 -0
  92. package/src/provider-usage.ts +64 -0
  93. package/src/repository-snapshots.ts +259 -0
  94. package/src/service.ts +446 -11
  95. package/src/store.ts +60 -4
  96. package/src/supervisor.ts +294 -41
  97. package/src/task-budget.ts +24 -11
  98. package/src/task-context.ts +17 -0
  99. package/src/task-driver.ts +51 -15
  100. package/src/task-policy.ts +23 -3
  101. package/src/task-verification.ts +2 -0
  102. package/src/task-work.ts +46 -1
  103. package/src/team-work.ts +554 -0
  104. package/src/tui-client.ts +5 -0
  105. package/src/tui-view.ts +164 -12
  106. package/src/tui.ts +5 -2
  107. package/src/usage.ts +17 -0
  108. package/src/worker-boundary.ts +395 -0
  109. package/src/worker-workspaces.ts +390 -0
  110. package/src/workspaces.ts +4 -2
@@ -0,0 +1,289 @@
1
+ import { constants, openSync, closeSync, readFileSync, fstatSync, lstatSync, existsSync, readdirSync, } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { canonical, digest, integer, object, ServiceError, string, textDigest, } from './protocol.js';
5
+ import { providerRoute } from './provider-config.js';
6
+ import { DIAGNOSTIC_PROFILE } from './worker-boundary.js';
7
+ const ROLE_TOOLS = {
8
+ explorer: ['salaam_probe', 'salaam_repository'],
9
+ reviewer: ['salaam_probe', 'salaam_repository', 'salaam_findings'],
10
+ builder: ['salaam_probe', 'salaam_repository', 'salaam_files', 'salaam_command'],
11
+ tester: ['salaam_probe', 'salaam_repository', 'salaam_command', 'salaam_findings'],
12
+ integrator: ['salaam_probe', 'salaam_repository', 'salaam_findings'],
13
+ };
14
+ export function profileTools(role) {
15
+ return [...ROLE_TOOLS[role]].sort();
16
+ }
17
+ export const BUILTIN_PROFILES = [
18
+ DIAGNOSTIC_PROFILE,
19
+ ...['explorer', 'reviewer', 'builder', 'tester', 'integrator'].map((role) => ({
20
+ schema_version: 1,
21
+ id: 'salaam/' + role,
22
+ revision: 1,
23
+ role,
24
+ tools: profileTools(role),
25
+ max_depth: 1,
26
+ max_requests: 8,
27
+ description: role === 'explorer'
28
+ ? 'Bounded evidence from an immutable repository snapshot'
29
+ : role === 'reviewer'
30
+ ? 'Independent findings over an immutable repository diff'
31
+ : role === 'builder'
32
+ ? 'Implement a proposal in a private owned workspace'
33
+ : role === 'tester'
34
+ ? 'Run bounded checks in an isolated disposable copy'
35
+ : 'Review integration evidence without applying changes',
36
+ prompt: role === 'explorer'
37
+ ? 'Investigate only the supplied repository snapshot. Cite actual file paths, line numbers and hashes. Do not claim edits or execution.'
38
+ : role === 'builder'
39
+ ? 'Implement only the assigned change in your private owned workspace using salaam_files. Run bounded checks through salaam_command. Command edits are disposable. Do not claim the root workspace was changed or verified; return a proposal for explicit integration.'
40
+ : role === 'tester'
41
+ ? 'Inspect the admitted snapshot and run bounded checks using salaam_command. You cannot edit persistent files. Report actual outcomes; a model answer is not a root verification receipt.'
42
+ : 'Review only the supplied immutable diff and repository snapshot. Report concrete findings with file/line evidence through salaam_findings. A finding is analysis, not a verification receipt.',
43
+ })),
44
+ ];
45
+ /** JSON front matter is a deliberately bounded subset, not an executable config format. */
46
+ export function parseAgentProfile(name, source) {
47
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(name) || Buffer.byteLength(source) > 49152)
48
+ throw new ServiceError('invalid_profile', 'Invalid profile name or size');
49
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(source);
50
+ if (!match)
51
+ throw new ServiceError('invalid_profile', 'Use JSON front matter between --- lines');
52
+ let metadata;
53
+ try {
54
+ metadata = object(JSON.parse(match[1]));
55
+ }
56
+ catch {
57
+ throw new ServiceError('invalid_profile', 'Invalid JSON profile metadata');
58
+ }
59
+ const allowed = [
60
+ 'description',
61
+ 'revision',
62
+ 'role',
63
+ 'tools',
64
+ 'provider',
65
+ 'model',
66
+ 'max_requests',
67
+ 'max_depth',
68
+ ];
69
+ if (Object.keys(metadata).some((key) => !allowed.includes(key)))
70
+ throw new ServiceError('invalid_profile', 'Profiles cannot define executables, credentials, plugins, environment or additional fields');
71
+ const role = string(metadata.role, 'role', 32);
72
+ if (!Object.hasOwn(ROLE_TOOLS, role))
73
+ throw new ServiceError('invalid_profile', 'Unsupported specialist role');
74
+ const tools = metadata.tools === undefined
75
+ ? profileTools(role)
76
+ : [
77
+ ...new Set((Array.isArray(metadata.tools) ? metadata.tools : []).map((x) => string(x, 'tool', 128))),
78
+ ].sort();
79
+ if (!tools.length || tools.some((x) => !profileTools(role).includes(x)))
80
+ throw new ServiceError('invalid_profile', 'Tools exceed the specialist role');
81
+ const revision = integer(metadata.revision ?? 1, 'revision');
82
+ const maxRequests = integer(metadata.max_requests ?? 8, 'max_requests');
83
+ if (revision < 1 || maxRequests < 1 || maxRequests > 8 || (metadata.max_depth ?? 1) !== 1)
84
+ throw new ServiceError('invalid_profile', 'Invalid revision, request limit or delegation depth');
85
+ if ((metadata.provider === undefined) !== (metadata.model === undefined))
86
+ throw new ServiceError('invalid_profile', 'Select both provider and model');
87
+ const prompt = string(match[2].trim(), 'prompt', 32768);
88
+ return {
89
+ schema_version: 1,
90
+ id: 'project/' + name,
91
+ revision,
92
+ role,
93
+ tools,
94
+ max_depth: 1,
95
+ description: string(metadata.description ?? name, 'description', 512),
96
+ prompt,
97
+ max_requests: maxRequests,
98
+ source_digest: textDigest(source),
99
+ ...(metadata.provider
100
+ ? {
101
+ provider: string(metadata.provider, 'provider', 128),
102
+ model: string(metadata.model, 'model', 256),
103
+ }
104
+ : {}),
105
+ };
106
+ }
107
+ export class AgentProfiles {
108
+ store;
109
+ route;
110
+ prepareRoute;
111
+ assertExecution;
112
+ validateFrame;
113
+ constructor(store, route, prepareRoute) {
114
+ this.store = store;
115
+ this.route = route;
116
+ this.prepareRoute = prepareRoute;
117
+ }
118
+ list(session) {
119
+ const root = this.store.session(session).cwd;
120
+ const control = join(root, '.salaam'), directory = join(control, 'agents');
121
+ for (const path of [control, directory]) {
122
+ if (!existsSync(path))
123
+ return structuredClone(BUILTIN_PROFILES);
124
+ const stat = lstatSync(path);
125
+ if (!stat.isDirectory() || stat.isSymbolicLink())
126
+ throw new ServiceError('invalid_profile', 'Profile directories must not be symlinks');
127
+ }
128
+ const names = readdirSync(directory)
129
+ .filter((name) => name.endsWith('.md'))
130
+ .sort();
131
+ if (names.length > 32)
132
+ throw new ServiceError('invalid_profile', 'At most 32 project profiles');
133
+ const profiles = names.map((file) => {
134
+ const path = join(directory, file), fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
135
+ try {
136
+ const stat = fstatSync(fd);
137
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 49152)
138
+ throw new ServiceError('invalid_profile', 'Profile must be a bounded regular file');
139
+ return parseAgentProfile(file.slice(0, -3), readFileSync(fd, 'utf8'));
140
+ }
141
+ finally {
142
+ closeSync(fd);
143
+ }
144
+ });
145
+ return [...structuredClone(BUILTIN_PROFILES), ...profiles];
146
+ }
147
+ inspect(session, id) {
148
+ this.store.session(session);
149
+ const builtin = BUILTIN_PROFILES.find((profile) => profile.id === id);
150
+ if (builtin)
151
+ return structuredClone(builtin);
152
+ const profile = this.list(session).find((x) => x.id === id);
153
+ if (!profile)
154
+ throw new ServiceError('not_found', 'Agent profile not found', 404);
155
+ return profile;
156
+ }
157
+ async admit(session, request, profileId, expectedDigest, factIds, controller) {
158
+ const input = { kind: 'agent-admit', session, profileId, expectedDigest, factIds };
159
+ const saved = this.store.cachedRequest('local-operator', request, input);
160
+ if (saved)
161
+ return saved;
162
+ this.store.authorizeControl(session, controller);
163
+ const profile = this.inspect(session, profileId);
164
+ if (digest(profile) !== expectedDigest)
165
+ throw new ServiceError('profile_changed', 'Review the current profile digest');
166
+ const writing = profile.tools.some((tool) => ['salaam_files', 'salaam_command'].includes(tool));
167
+ if (writing) {
168
+ if (!this.assertExecution)
169
+ throw new ServiceError('profile_unavailable', 'Writing roles require the parallel execution boundary');
170
+ this.assertExecution();
171
+ }
172
+ const prepared = await this.prepareRoute(profile.provider
173
+ ? providerRoute(profile.provider, profile.model, Math.min(2048, this.route.max_tokens))
174
+ : this.route, session);
175
+ return this.store.request('local-operator', request, input, () => {
176
+ this.store.authorizeControl(session, controller);
177
+ if (writing)
178
+ this.assertExecution();
179
+ if (digest(this.inspect(session, profileId)) !== expectedDigest)
180
+ throw new ServiceError('profile_changed', 'Profile changed during admission');
181
+ if (profile.role &&
182
+ !this.store.db.prepare('SELECT id FROM workspaces WHERE session_id=?').get(session))
183
+ throw new ServiceError('workspace_unavailable', 'Specialists need an admitted coding workspace');
184
+ if (factIds.length > 16 || new Set(factIds).size !== factIds.length)
185
+ throw new ServiceError('invalid_profile', 'Select at most 16 distinct facts');
186
+ const facts = factIds.map((id) => {
187
+ const fact = this.store.db
188
+ .prepare('SELECT id,kind,text FROM task_facts WHERE id=? AND session_id=? AND id NOT IN (SELECT supersedes FROM task_facts WHERE supersedes IS NOT NULL)')
189
+ .get(id, session);
190
+ if (!fact)
191
+ throw new ServiceError('not_found', 'Selected fact is unavailable', 404);
192
+ return { id: String(fact.id), kind: String(fact.kind), text: String(fact.text) };
193
+ });
194
+ const grant = {
195
+ schema_version: 1,
196
+ id: randomUUID(),
197
+ owner_session: session,
198
+ profile,
199
+ profile_digest: expectedDigest,
200
+ route: prepared.route,
201
+ output_cap: prepared.output_cap,
202
+ facts,
203
+ };
204
+ const priorProfile = this.store.db
205
+ .prepare('SELECT digest FROM worker_profiles WHERE id=? AND revision=?')
206
+ .get(profile.id, profile.revision);
207
+ if (priorProfile && priorProfile.digest !== expectedDigest)
208
+ throw new ServiceError('profile_revision_conflict', 'Changed profile contents require a new revision');
209
+ this.store.db
210
+ .prepare('INSERT INTO agent_grants(id,owner_session,profile_id,document,digest,state) VALUES (?,?,?,?,?,?)')
211
+ .run(grant.id, session, profileId, canonical(grant), digest(grant), 'active');
212
+ this.store.db
213
+ .prepare('INSERT OR IGNORE INTO worker_profiles(id,revision,digest,document) VALUES (?,?,?,?)')
214
+ .run(profile.id, profile.revision, expectedDigest, canonical(profile));
215
+ this.store.event(session, 'agent.admitted', {
216
+ grant_id: grant.id,
217
+ profile_id: profileId,
218
+ profile_digest: expectedDigest,
219
+ route: prepared.route,
220
+ });
221
+ this.validateFrame?.(session);
222
+ return grant;
223
+ });
224
+ }
225
+ resolve(session, id) {
226
+ const row = this.store.db
227
+ .prepare("SELECT document,digest FROM agent_grants WHERE owner_session=? AND profile_id=? AND state='active' ORDER BY rowid DESC LIMIT 1")
228
+ .get(session, id);
229
+ if (!row)
230
+ throw new ServiceError('authority_denied', 'Operator must admit this agent profile first');
231
+ const grant = JSON.parse(String(row.document));
232
+ if (digest(grant) !== row.digest || digest(this.inspect(session, id)) !== grant.profile_digest)
233
+ throw new ServiceError('profile_changed', 'Profile changed; renew its admission');
234
+ return grant;
235
+ }
236
+ validate(grant) {
237
+ const row = this.store.db
238
+ .prepare('SELECT document,digest,state FROM agent_grants WHERE id=? AND owner_session=?')
239
+ .get(grant.id, grant.owner_session);
240
+ if (!row ||
241
+ row.state !== 'active' ||
242
+ row.digest !== digest(grant) ||
243
+ row.document !== canonical(grant) ||
244
+ digest(this.inspect(grant.owner_session, grant.profile.id)) !== grant.profile_digest)
245
+ throw new ServiceError('worker_profile_revoked', 'Specialist admission was revoked or changed');
246
+ }
247
+ revoke(session, grantId, request, controller) {
248
+ return this.store.request('local-operator', request, { kind: 'agent-revoke', session, grantId }, () => {
249
+ this.store.authorizeControl(session, controller);
250
+ const result = this.store.db
251
+ .prepare("UPDATE agent_grants SET state='revoked' WHERE id=? AND owner_session=?")
252
+ .run(grantId, session);
253
+ if (!result.changes)
254
+ throw new ServiceError('not_found', 'Agent admission not found', 404);
255
+ this.store.event(session, 'agent.revoked', { grant_id: grantId });
256
+ return { grant_id: grantId, state: 'revoked' };
257
+ });
258
+ }
259
+ snapshot(session) {
260
+ return this.store.db
261
+ .prepare('SELECT document,state FROM agent_grants WHERE owner_session=? ORDER BY rowid DESC LIMIT 64')
262
+ .all(session)
263
+ .reverse()
264
+ .map((row) => {
265
+ const grant = JSON.parse(String(row.document));
266
+ let state = String(row.state);
267
+ if (state === 'active') {
268
+ try {
269
+ this.validate(grant);
270
+ }
271
+ catch {
272
+ state = 'stale';
273
+ }
274
+ }
275
+ return {
276
+ grant_id: grant.id,
277
+ profile_id: grant.profile.id,
278
+ revision: grant.profile.revision,
279
+ profile_digest: grant.profile_digest,
280
+ tools: grant.profile.tools,
281
+ role: grant.profile.role,
282
+ route: grant.route,
283
+ output_cap: grant.output_cap,
284
+ selected_facts: grant.facts.map((f) => f.id),
285
+ state,
286
+ };
287
+ });
288
+ }
289
+ }
@@ -4,12 +4,16 @@ export class ChildWork {
4
4
  store;
5
5
  artifacts;
6
6
  supervisor;
7
+ repositories;
8
+ external;
9
+ workspaces;
7
10
  paused = false;
8
11
  starts = new Set();
9
- constructor(store, artifacts, supervisor) {
12
+ constructor(store, artifacts, supervisor, repositories) {
10
13
  this.store = store;
11
14
  this.artifacts = artifacts;
12
15
  this.supervisor = supervisor;
16
+ this.repositories = repositories;
13
17
  store.finishChild = (attempt) => this.finish(attempt);
14
18
  store.cancelChildren = (attempt) => this.cancelIntents(attempt);
15
19
  }
@@ -21,7 +25,7 @@ export class ChildWork {
21
25
  list(parent) {
22
26
  return this.store.db
23
27
  .prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid')
24
- .all(parent);
28
+ .all(parent).map((child) => ({ ...child, worker: this.store.workerBoundary.view(child.id) }));
25
29
  }
26
30
  inspect(parent, id) {
27
31
  const row = this.store.db
@@ -29,9 +33,9 @@ export class ChildWork {
29
33
  .get(id, parent);
30
34
  if (!row)
31
35
  throw new ServiceError('not_found', 'Child task not found', 404);
32
- return row;
36
+ return { ...row, worker: this.store.workerBoundary.view(row.id) };
33
37
  }
34
- spawn(parent, epoch, attempt, step, prompt, budget, requested) {
38
+ spawn(parent, epoch, attempt, step, prompt, budget, requested, profileId, admission) {
35
39
  if (!Number.isSafeInteger(budget) || budget < 1 || budget > 8)
36
40
  throw new ServiceError('invalid_budget', 'Child request budget must be 1–8');
37
41
  if (step.length > 128 ||
@@ -41,12 +45,18 @@ export class ChildWork {
41
45
  prompt.trimStart().startsWith('/'))
42
46
  throw new ServiceError('invalid_child', 'Invalid child step or prompt');
43
47
  // Current child authority is deliberately read-only and closed-world.
44
- const authority = requested
45
- .filter((x) => x === 'salaam_probe')
46
- .filter((x, i, a) => a.indexOf(x) === i)
47
- .sort();
48
- const input = digest({ prompt, budget, requested: [...new Set(requested)].sort() });
49
- const child = this.store.transaction(() => {
48
+ const input = digest({
49
+ prompt,
50
+ budget,
51
+ requested: [...new Set(requested)].sort(),
52
+ ...(profileId ? { profile: profileId } : {}),
53
+ ...(admission ? { assignment: admission.assignment } : {}),
54
+ ...(admission?.runtime ? { runtime: admission.runtime } : {}),
55
+ });
56
+ let fresh = false;
57
+ if (this.store.db.isTransaction && !admission?.defer)
58
+ throw new Error('Nested child dispatch requires an owned deferred admission');
59
+ const prepare = () => {
50
60
  const old = this.store.db
51
61
  .prepare('SELECT * FROM child_tasks WHERE parent_session=? AND step_key=?')
52
62
  .get(parent, step);
@@ -57,34 +67,83 @@ export class ChildWork {
57
67
  }
58
68
  if (this.paused)
59
69
  throw new ServiceError('stopping', 'Child dispatch is stopping');
60
- this.supervisor.assertActive(parent, epoch, attempt);
70
+ if (admission)
71
+ admission.authorize();
72
+ else
73
+ this.supervisor.assertActive(parent, epoch, attempt);
74
+ const agent = profileId
75
+ ? this.store.workerBoundary.profiles?.resolve(parent, profileId)
76
+ : undefined;
77
+ if (profileId && !agent)
78
+ throw new ServiceError('authority_denied', 'Agent profile is unavailable');
79
+ if (!admission?.runtime)
80
+ this.supervisor.assertSessionProviderSupported(parent, agent?.output_cap);
81
+ if (!admission?.runtime && agent && budget > (agent.profile.max_requests ?? 8))
82
+ throw new ServiceError('invalid_budget', 'Budget exceeds the admitted profile limit');
83
+ const available = [
84
+ ...(agent?.profile.tools ?? ['salaam_probe']),
85
+ ...(admission && !admission.runtime ? ['salaam_team'] : []),
86
+ ];
87
+ const authority = [
88
+ ...new Set(agent && !requested.length ? available : requested.filter((x) => available.includes(x))),
89
+ ].sort();
90
+ const effectivePrompt = agent
91
+ ? `${agent.profile.prompt ?? 'Run the diagnostic probe.'}${admission?.runtime ? '\n\nRuntime binding: use the admitted external harness tools for the corresponding file and command actions. Its private draft is /workspace. Return evidence for explicit root integration; this invocation cannot verify the root task.' : ''}\n\nGoal:\n${prompt}\n\nExplicitly selected facts:\n${JSON.stringify(agent.facts)}`
92
+ : prompt;
93
+ if (Buffer.byteLength(effectivePrompt) > 65536)
94
+ throw new ServiceError('invalid_child', 'Specialist context exceeds 64 KiB');
61
95
  if (this.owned(parent))
62
96
  throw new ServiceError('depth_exceeded', 'Child depth is limited to one');
63
97
  if (!authority.length)
64
98
  throw new ServiceError('authority_denied', 'No requested read-only authority is available');
65
- if (this.list(parent).filter((x) => x.state === 'accepted').length >= 2)
99
+ if (this.activeCount(parent) >= 2)
66
100
  throw new ServiceError('child_limit', 'At most two children may be active');
67
- this.store.usage.budget(attempt);
68
- const changed = this.store.db
69
- .prepare('UPDATE attempt_budgets SET reserved_requests=reserved_requests+? WHERE attempt_id=? AND spent_requests+reserved_requests+?<=limit_requests')
70
- .run(budget, attempt, budget);
71
- if (!changed.changes)
72
- throw new ServiceError('budget_exhausted', 'Parent cannot reserve this child budget');
101
+ if (this.activeCount() >= 8)
102
+ throw new ServiceError('child_limit', 'Daemon worker admission limit reached');
103
+ if (!admission?.runtime) {
104
+ this.store.usage.budget(attempt);
105
+ const changed = this.store.db
106
+ .prepare('UPDATE attempt_budgets SET reserved_requests=reserved_requests+? WHERE attempt_id=? AND spent_requests+reserved_requests+?<=limit_requests')
107
+ .run(budget, attempt, budget);
108
+ if (!changed.changes)
109
+ throw new ServiceError('budget_exhausted', 'Parent cannot reserve this child budget');
110
+ }
73
111
  const id = randomUUID(), session = randomUUID(), submission = randomUUID(), childAttempt = randomUUID();
74
112
  this.store.db
75
113
  .prepare('INSERT INTO sessions(id,cwd,pi_session_id) VALUES (?,?,?)')
76
114
  .run(session, this.store.session(parent).cwd, session);
77
115
  this.store.db
78
116
  .prepare('INSERT INTO submissions(id,session_id,text,attempt_id) VALUES (?,?,?,?)')
79
- .run(submission, session, prompt, childAttempt);
117
+ .run(submission, session, effectivePrompt, childAttempt);
80
118
  this.store.db
81
119
  .prepare("INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'accepted')")
82
120
  .run(childAttempt, session, submission);
83
121
  this.store.db
84
122
  .prepare("INSERT INTO child_tasks(id,parent_session,parent_attempt,step_key,input_digest,child_session,child_attempt,authority,budget,state) VALUES (?,?,?,?,?,?,?,?,?,'accepted')")
85
123
  .run(id, parent, attempt, step, input, session, childAttempt, JSON.stringify(authority), budget);
86
- this.store.taskChildReserve?.(parent, childAttempt, budget);
87
- this.store.usage.budget(childAttempt, budget);
124
+ if (!admission?.runtime) {
125
+ this.store.taskChildReserve?.(parent, childAttempt, budget);
126
+ this.store.usage.budget(childAttempt, budget);
127
+ }
128
+ const artifact = this.artifacts.prepare(session, effectivePrompt);
129
+ artifact.commit();
130
+ const repository = agent?.profile.role
131
+ ? this.repositories?.capture(parent, session)
132
+ : undefined;
133
+ if (agent?.profile.role && !repository)
134
+ throw new ServiceError('workspace_unavailable', 'No specialist snapshot boundary');
135
+ const writing = authority.some((tool) => ['salaam_files', 'salaam_command'].includes(tool));
136
+ const isolation = (writing || admission?.runtime) && repository
137
+ ? this.workspaces?.prepare(session, repository)
138
+ : undefined;
139
+ if (writing && !isolation)
140
+ throw new ServiceError('authority_denied', 'No isolated coding boundary');
141
+ this.store.workerBoundary.freeze(this.owned(session), artifact.id, false, agent, repository, admission?.assignment, isolation, admission?.runtime);
142
+ if (admission?.runtime) {
143
+ if (!this.external)
144
+ throw new ServiceError('runtime_unavailable', 'External runtime is unavailable');
145
+ this.external.prepare(this.owned(session));
146
+ }
88
147
  this.store.event(session, 'session.created', { parent_session: parent, child_id: id });
89
148
  this.store.event(session, 'submission.accepted', {
90
149
  attempt_id: childAttempt,
@@ -97,17 +156,37 @@ export class ChildWork {
97
156
  budget,
98
157
  authority,
99
158
  });
159
+ fresh = true;
100
160
  return this.inspect(parent, id);
101
- });
161
+ };
162
+ const child = this.store.db.isTransaction ? prepare() : this.store.transaction(prepare);
102
163
  // Repeated calls only return the durable binding. They never restart a child.
103
- if (this.store.session(child.child_session).epoch === 0 &&
164
+ if (fresh &&
165
+ !admission?.defer &&
166
+ this.store.session(child.child_session).epoch === 0 &&
104
167
  this.store.attempt(child.child_attempt).state === 'accepted')
105
168
  this.start(child);
106
169
  return child;
107
170
  }
171
+ activeCount(parent) {
172
+ return Number(this.store.db
173
+ .prepare(`SELECT count(*) AS n FROM child_tasks c WHERE (? IS NULL OR c.parent_session=?) AND (c.state='accepted' OR EXISTS(SELECT 1 FROM workers w WHERE w.session_id=c.child_session AND w.state!='exited') OR EXISTS(SELECT 1 FROM external_runs e WHERE e.child_id=c.id AND e.stopped=0))`)
174
+ .get(parent ?? null, parent ?? null).n);
175
+ }
176
+ launchPrepared(child) {
177
+ if (this.store.db.isTransaction)
178
+ throw new Error('Commit child intent before launch');
179
+ if (this.store.workerBoundary.view(child.id).dispatch_state === 'intent')
180
+ this.start(child);
181
+ }
182
+ cancelSessionInTransaction(parent) {
183
+ const child = this.list(parent)[0];
184
+ if (child)
185
+ this.cancelIntents(child.parent_attempt);
186
+ }
108
187
  start(child) {
109
- const task = this.supervisor
110
- .start(child.child_session)
188
+ const external = this.store.workerBoundary.view(child.id).binding.external;
189
+ const task = (external ? this.external.start(child) : this.supervisor.start(child.child_session))
111
190
  .catch((error) => {
112
191
  this.store.transaction(() => {
113
192
  const attempt = this.store.attempt(child.child_attempt);
@@ -119,6 +198,7 @@ export class ChildWork {
119
198
  this.store.db
120
199
  .prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
121
200
  .run(attempt.submission_id);
201
+ this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
122
202
  this.finish(this.store.attempt(attempt.id));
123
203
  });
124
204
  })
@@ -143,6 +223,7 @@ export class ChildWork {
143
223
  this.store.db
144
224
  .prepare('UPDATE submissions SET state=? WHERE id=?')
145
225
  .run(state, attempt.submission_id);
226
+ this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
146
227
  this.finish(this.store.attempt(attempt.id));
147
228
  });
148
229
  }
@@ -159,12 +240,22 @@ export class ChildWork {
159
240
  state: attempt.state,
160
241
  text: attempt.result,
161
242
  error: attempt.error,
243
+ ...(this.store.workerBoundary.view(child.id).binding.agent
244
+ ? {
245
+ worker_id: this.store.workerBoundary.view(child.id).binding.worker_id,
246
+ report: this.store.db
247
+ .prepare('SELECT artifact_id FROM specialist_reports WHERE worker_id=?')
248
+ .get(this.store.workerBoundary.view(child.id).binding.worker_id)?.artifact_id ??
249
+ null,
250
+ }
251
+ : {}),
162
252
  }));
163
253
  artifact.commit();
164
254
  const result = randomUUID();
165
255
  this.store.db
166
256
  .prepare('UPDATE child_tasks SET state=?,result_id=?,artifact_id=? WHERE id=?')
167
257
  .run(attempt.state, result, artifact.id, child.id);
258
+ this.store.workerBoundary.finish(child, attempt.state === 'interrupted');
168
259
  this.store.event(child.parent_session, 'child.result_available', {
169
260
  child_id: child.id,
170
261
  result_id: result,
@@ -208,6 +299,7 @@ export class ChildWork {
208
299
  this.store.db
209
300
  .prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
210
301
  .run(attempt.submission_id);
302
+ this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
211
303
  }
212
304
  this.finish(this.store.attempt(attempt.id));
213
305
  }
@@ -226,6 +318,7 @@ export class ChildWork {
226
318
  this.store.db
227
319
  .prepare("UPDATE submissions SET state='cancelled' WHERE id=?")
228
320
  .run(work.submission_id);
321
+ this.store.projectTaskAttempt?.(this.store.attempt(work.id));
229
322
  this.finish(this.store.attempt(work.id));
230
323
  }
231
324
  this.store.event(child.child_session, 'attempt.cancel_requested', {
@@ -238,9 +331,22 @@ export class ChildWork {
238
331
  const children = this.store.db
239
332
  .prepare('SELECT * FROM child_tasks WHERE parent_session=?')
240
333
  .all(this.store.attempt(attempt).session_id);
334
+ for (const child of children.filter((child) => this.store.cancelled(child.child_attempt)))
335
+ for (const run of this.supervisor.coding?.list(child.child_session) ?? [])
336
+ if (![
337
+ 'succeeded',
338
+ 'failed',
339
+ 'cancelled',
340
+ 'timed_out',
341
+ 'output_limited',
342
+ 'interrupted',
343
+ ].includes(run.state))
344
+ this.supervisor.coding.cancel(child.child_session, run.id, 'child-stop:' + run.id, () => { });
241
345
  await Promise.all(children
242
346
  .filter((child) => this.store.cancelled(child.child_attempt))
243
- .map((child) => this.supervisor.cancelAttempt(child.child_attempt)));
347
+ .map((child) => this.store.workerBoundary.view(child.id).binding.external
348
+ ? this.external.cancel(child)
349
+ : this.supervisor.cancelAttempt(child.child_attempt)));
244
350
  }
245
351
  pause() {
246
352
  this.paused = true;