@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
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Store } from './store.js';
|
|
3
|
+
import type { ChildWork } from './children.js';
|
|
4
|
+
import type { Artifacts } from './artifacts.js';
|
|
5
|
+
import { readTree, type Workspaces, type WorkspaceTree } from './workspaces.js';
|
|
6
|
+
import { repositoryPath, type RepositorySnapshots } from './repository-snapshots.js';
|
|
7
|
+
import { canonical, digest, ServiceError } from './protocol.js';
|
|
8
|
+
import type { WorkerBinding } from './worker-boundary.js';
|
|
9
|
+
import type { CodingWork } from './coding.js';
|
|
10
|
+
import { workspaceDiff } from './workspace-diff.js';
|
|
11
|
+
|
|
12
|
+
/** Drafts are private owned copies. Integration reuses the hash-bound workspace journal. */
|
|
13
|
+
export class WorkerWorkspaces {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly store: Store,
|
|
16
|
+
readonly workspaces: Workspaces,
|
|
17
|
+
readonly repositories: RepositorySnapshots,
|
|
18
|
+
readonly artifacts: Artifacts,
|
|
19
|
+
readonly coding: CodingWork,
|
|
20
|
+
readonly children: ChildWork,
|
|
21
|
+
) {}
|
|
22
|
+
execution(): { profile_digest: string; engine_id: string; evidence: 'proven' | 'fixture' } {
|
|
23
|
+
const executor = this.coding.executor;
|
|
24
|
+
if (
|
|
25
|
+
!executor?.profileDigest ||
|
|
26
|
+
!executor.engineId ||
|
|
27
|
+
(executor.evidence.probe !== 'passed' && executor.evidence.fixture !== true)
|
|
28
|
+
)
|
|
29
|
+
throw new ServiceError(
|
|
30
|
+
'executor_unavailable',
|
|
31
|
+
'Writing specialists require a configured execution boundary',
|
|
32
|
+
);
|
|
33
|
+
return {
|
|
34
|
+
profile_digest: executor.profileDigest,
|
|
35
|
+
engine_id: executor.engineId,
|
|
36
|
+
evidence: executor.evidence.fixture === true ? 'fixture' : 'proven',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
prepare(
|
|
40
|
+
session: string,
|
|
41
|
+
repository: NonNullable<WorkerBinding['repository']>,
|
|
42
|
+
): {
|
|
43
|
+
workspace_id: string;
|
|
44
|
+
baseline_artifact: string;
|
|
45
|
+
digest: string;
|
|
46
|
+
execution: ReturnType<WorkerWorkspaces['execution']>;
|
|
47
|
+
} {
|
|
48
|
+
const execution = this.execution();
|
|
49
|
+
const tree = JSON.parse(this.artifacts.read(repository.artifact_id)) as WorkspaceTree;
|
|
50
|
+
if (digest(tree) !== repository.digest)
|
|
51
|
+
throw new ServiceError('artifact_corrupt', 'Worker baseline digest changed');
|
|
52
|
+
const workspace = this.workspaces.cloneSnapshot(session, tree);
|
|
53
|
+
return {
|
|
54
|
+
workspace_id: workspace.id,
|
|
55
|
+
baseline_artifact: repository.artifact_id,
|
|
56
|
+
digest: repository.digest,
|
|
57
|
+
execution,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
assert(binding: WorkerBinding): void {
|
|
61
|
+
const scope = binding.isolation;
|
|
62
|
+
if (!scope || canonical(scope.execution) !== canonical(this.execution()))
|
|
63
|
+
throw new ServiceError('worker_binding_changed', 'Worker execution environment changed');
|
|
64
|
+
const workspace = this.workspaces.get(binding.session_id);
|
|
65
|
+
if (
|
|
66
|
+
workspace.id !== scope.workspace_id ||
|
|
67
|
+
scope.digest !== binding.repository?.digest ||
|
|
68
|
+
scope.baseline_artifact !== binding.repository?.artifact_id
|
|
69
|
+
)
|
|
70
|
+
throw new ServiceError('worker_binding_changed', 'Worker workspace ownership changed');
|
|
71
|
+
this.coding.assertSafe(binding.session_id);
|
|
72
|
+
}
|
|
73
|
+
private settled(parent: string, id: string, complete = true) {
|
|
74
|
+
const child = this.children.inspect(parent, id),
|
|
75
|
+
binding = child.worker!.binding;
|
|
76
|
+
if (
|
|
77
|
+
!binding.isolation ||
|
|
78
|
+
(binding.external && !this.children.external?.stopped(child.id)) ||
|
|
79
|
+
(complete ? child.state !== 'completed' : child.state === 'accepted') ||
|
|
80
|
+
this.store.db
|
|
81
|
+
.prepare("SELECT 1 FROM workers WHERE session_id=? AND state!='exited'")
|
|
82
|
+
.get(child.child_session) ||
|
|
83
|
+
this.store.db
|
|
84
|
+
.prepare(
|
|
85
|
+
"SELECT 1 FROM command_runs WHERE session_id=? AND state NOT IN ('succeeded','failed','cancelled','timed_out','output_limited','interrupted')",
|
|
86
|
+
)
|
|
87
|
+
.get(child.child_session)
|
|
88
|
+
)
|
|
89
|
+
throw new ServiceError(
|
|
90
|
+
'worker_not_settled',
|
|
91
|
+
'Wait for the owned worker and commands to stop before inspecting its proposal',
|
|
92
|
+
);
|
|
93
|
+
this.assert(binding);
|
|
94
|
+
return { child, binding };
|
|
95
|
+
}
|
|
96
|
+
proposal(parent: string, id: string, offset?: number): unknown {
|
|
97
|
+
const { child, binding } = this.settled(parent, id, false);
|
|
98
|
+
const workspace = this.workspaces.verify(child.child_session),
|
|
99
|
+
baseline = this.repositories.tree(binding),
|
|
100
|
+
current = readTree(workspace.root);
|
|
101
|
+
if (digest(current) !== workspace.digest)
|
|
102
|
+
throw new ServiceError('workspace_changed', 'Worker draft changed during inspection');
|
|
103
|
+
const changes = [...new Set([...Object.keys(baseline), ...Object.keys(current)])]
|
|
104
|
+
.sort()
|
|
105
|
+
.filter((path) => digest(baseline[path] ?? null) !== digest(current[path] ?? null))
|
|
106
|
+
.map((path) => {
|
|
107
|
+
repositoryPath(path);
|
|
108
|
+
return { path, before: baseline[path] ?? null, after: current[path] ?? null };
|
|
109
|
+
});
|
|
110
|
+
const patch = {
|
|
111
|
+
schema_version: 1,
|
|
112
|
+
worker_id: binding.worker_id,
|
|
113
|
+
child_id: id,
|
|
114
|
+
worker_state: child.state,
|
|
115
|
+
baseline_digest: binding.repository!.digest,
|
|
116
|
+
draft_digest: workspace.digest,
|
|
117
|
+
changes,
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
...patch,
|
|
121
|
+
changes: offset === undefined ? changes : changes.slice(offset, offset + 1),
|
|
122
|
+
diff: (offset === undefined ? changes : changes.slice(offset, offset + 1)).map(workspaceDiff),
|
|
123
|
+
next_offset: offset !== undefined && offset + 1 < changes.length ? offset + 1 : null,
|
|
124
|
+
patch_digest: digest(patch),
|
|
125
|
+
verified_success: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
integrate(
|
|
129
|
+
parent: string,
|
|
130
|
+
childId: string,
|
|
131
|
+
request: string,
|
|
132
|
+
revision: number,
|
|
133
|
+
rootDigest: string,
|
|
134
|
+
patchDigest: string,
|
|
135
|
+
authorize: () => void,
|
|
136
|
+
): unknown {
|
|
137
|
+
let fresh = false;
|
|
138
|
+
const receipt = this.store.request(
|
|
139
|
+
'local-operator',
|
|
140
|
+
request,
|
|
141
|
+
{ kind: 'worker-integrate', parent, childId, revision, rootDigest, patchDigest },
|
|
142
|
+
() => {
|
|
143
|
+
authorize();
|
|
144
|
+
const task = this.store.db
|
|
145
|
+
.prepare('SELECT state FROM durable_tasks WHERE session_id=?')
|
|
146
|
+
.get(parent);
|
|
147
|
+
if (task?.state === 'cancelled')
|
|
148
|
+
throw new ServiceError('task_settled', 'Task cancellation revoked integration authority');
|
|
149
|
+
const { child, binding } = this.settled(parent, childId);
|
|
150
|
+
this.assertRootIdle(parent);
|
|
151
|
+
if (
|
|
152
|
+
this.store.db.prepare('SELECT 1 FROM worker_integrations WHERE child_id=?').get(childId)
|
|
153
|
+
)
|
|
154
|
+
throw new ServiceError(
|
|
155
|
+
'integration_exists',
|
|
156
|
+
'This draft already has an integration intent; inspect its receipt',
|
|
157
|
+
);
|
|
158
|
+
const proposal = this.proposal(parent, childId) as {
|
|
159
|
+
patch_digest: string;
|
|
160
|
+
changes: {
|
|
161
|
+
path: string;
|
|
162
|
+
before: WorkspaceTree[string] | null;
|
|
163
|
+
after: WorkspaceTree[string] | null;
|
|
164
|
+
}[];
|
|
165
|
+
};
|
|
166
|
+
if (proposal.patch_digest !== patchDigest)
|
|
167
|
+
throw new ServiceError('proposal_changed', 'Review the exact current worker proposal');
|
|
168
|
+
if (
|
|
169
|
+
this.store.db
|
|
170
|
+
.prepare(
|
|
171
|
+
"SELECT 1 FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running')",
|
|
172
|
+
)
|
|
173
|
+
.get(parent)
|
|
174
|
+
)
|
|
175
|
+
throw new ServiceError('task_busy', 'Pause root work before integration');
|
|
176
|
+
const workspace = this.workspaces.verify(parent);
|
|
177
|
+
if (workspace.revision !== revision || workspace.digest !== rootDigest)
|
|
178
|
+
throw new ServiceError('workspace_changed', 'Root workspace changed since review');
|
|
179
|
+
const before = readTree(workspace.root),
|
|
180
|
+
after: WorkspaceTree = { ...before };
|
|
181
|
+
for (const change of proposal.changes) {
|
|
182
|
+
if (digest(before[change.path] ?? null) !== digest(change.before))
|
|
183
|
+
throw new ServiceError(
|
|
184
|
+
'integration_conflict',
|
|
185
|
+
'Root and worker both changed ' + change.path,
|
|
186
|
+
);
|
|
187
|
+
if (change.after) after[change.path] = change.after;
|
|
188
|
+
else delete after[change.path];
|
|
189
|
+
}
|
|
190
|
+
const artifact = this.artifacts.prepare(parent, JSON.stringify(after));
|
|
191
|
+
artifact.commit();
|
|
192
|
+
const id = randomUUID();
|
|
193
|
+
this.store.db
|
|
194
|
+
.prepare("INSERT INTO worker_integrations VALUES (?,?,?,?,?,?,?,?,'prepared',NULL,?,1)")
|
|
195
|
+
.run(
|
|
196
|
+
id,
|
|
197
|
+
parent,
|
|
198
|
+
child.id,
|
|
199
|
+
binding.worker_id,
|
|
200
|
+
patchDigest,
|
|
201
|
+
workspace.revision,
|
|
202
|
+
workspace.digest,
|
|
203
|
+
artifact.id,
|
|
204
|
+
'worker-integration:' + id,
|
|
205
|
+
);
|
|
206
|
+
this.store.event(parent, 'worker.integration_prepared', {
|
|
207
|
+
integration_id: id,
|
|
208
|
+
child_id: childId,
|
|
209
|
+
patch_digest: patchDigest,
|
|
210
|
+
before_digest: workspace.digest,
|
|
211
|
+
after_digest: digest(after),
|
|
212
|
+
});
|
|
213
|
+
fresh = true;
|
|
214
|
+
return { id };
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
if (fresh) this.apply(receipt.id, authorize);
|
|
218
|
+
return this.inspect(receipt.id);
|
|
219
|
+
}
|
|
220
|
+
private apply(id: string, authorize: () => void): void {
|
|
221
|
+
const row = this.row(id),
|
|
222
|
+
parent = String(row.parent_session),
|
|
223
|
+
workspace = this.workspaces.verify(parent),
|
|
224
|
+
before = readTree(workspace.root),
|
|
225
|
+
after = JSON.parse(this.artifacts.read(String(row.after_artifact))) as WorkspaceTree;
|
|
226
|
+
authorize();
|
|
227
|
+
this.assertRootIdle(parent);
|
|
228
|
+
if (workspace.revision !== row.before_revision || digest(before) !== row.before_digest)
|
|
229
|
+
throw new ServiceError(
|
|
230
|
+
'workspace_changed',
|
|
231
|
+
'Root changed after integration intent; no overwrite was performed',
|
|
232
|
+
);
|
|
233
|
+
this.workspaces.replace(
|
|
234
|
+
parent,
|
|
235
|
+
String(row.mutation_key),
|
|
236
|
+
String(row.patch_digest),
|
|
237
|
+
Number(row.before_revision),
|
|
238
|
+
before,
|
|
239
|
+
after,
|
|
240
|
+
);
|
|
241
|
+
this.reconcile(id);
|
|
242
|
+
}
|
|
243
|
+
resolve(
|
|
244
|
+
id: string,
|
|
245
|
+
request: string,
|
|
246
|
+
revision: number,
|
|
247
|
+
rootDigest: string,
|
|
248
|
+
authorize: () => void,
|
|
249
|
+
): unknown {
|
|
250
|
+
let fresh = false;
|
|
251
|
+
this.store.request(
|
|
252
|
+
'local-operator',
|
|
253
|
+
request,
|
|
254
|
+
{ kind: 'integration-resolve', id, revision, rootDigest },
|
|
255
|
+
() => {
|
|
256
|
+
authorize();
|
|
257
|
+
const row = this.row(id),
|
|
258
|
+
parent = String(row.parent_session),
|
|
259
|
+
workspace = this.workspaces.verify(parent);
|
|
260
|
+
this.assertRootIdle(parent);
|
|
261
|
+
const task = this.store.db
|
|
262
|
+
.prepare('SELECT state FROM durable_tasks WHERE session_id=?')
|
|
263
|
+
.get(parent);
|
|
264
|
+
if (task?.state === 'cancelled')
|
|
265
|
+
throw new ServiceError('task_settled', 'Task cancellation revoked integration authority');
|
|
266
|
+
if (
|
|
267
|
+
!['prepared', 'needs_resolution'].includes(String(row.state)) ||
|
|
268
|
+
Number(row.generation) >= 8
|
|
269
|
+
)
|
|
270
|
+
throw new ServiceError(
|
|
271
|
+
'integration_settled',
|
|
272
|
+
'Integration is applied or its resolution limit was reached',
|
|
273
|
+
);
|
|
274
|
+
if (
|
|
275
|
+
workspace.revision !== revision ||
|
|
276
|
+
workspace.digest !== rootDigest ||
|
|
277
|
+
revision !== row.before_revision ||
|
|
278
|
+
rootDigest !== row.before_digest
|
|
279
|
+
)
|
|
280
|
+
throw new ServiceError(
|
|
281
|
+
'workspace_changed',
|
|
282
|
+
'Resolution requires the exact unchanged pre-integration root',
|
|
283
|
+
);
|
|
284
|
+
const mutation = this.store.db
|
|
285
|
+
.prepare('SELECT state FROM workspace_mutations WHERE workspace_id=? AND request_key=?')
|
|
286
|
+
.get(workspace.id, String(row.mutation_key));
|
|
287
|
+
if (mutation && mutation.state !== 'failed_definite')
|
|
288
|
+
throw new ServiceError(
|
|
289
|
+
'integration_uncertain',
|
|
290
|
+
'Reconcile the existing workspace mutation before a new apply',
|
|
291
|
+
);
|
|
292
|
+
const generation = Number(row.generation) + 1,
|
|
293
|
+
key = 'worker-integration:' + id + ':' + generation;
|
|
294
|
+
this.store.db
|
|
295
|
+
.prepare(
|
|
296
|
+
"UPDATE worker_integrations SET state='prepared',mutation_id=NULL,mutation_key=?,generation=? WHERE id=?",
|
|
297
|
+
)
|
|
298
|
+
.run(key, generation, id);
|
|
299
|
+
this.store.event(parent, 'worker.integration_resolution_admitted', {
|
|
300
|
+
integration_id: id,
|
|
301
|
+
generation,
|
|
302
|
+
mutation_key: key,
|
|
303
|
+
request_id: request,
|
|
304
|
+
});
|
|
305
|
+
fresh = true;
|
|
306
|
+
return { id };
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
if (fresh) this.apply(id, authorize);
|
|
310
|
+
return this.inspect(id);
|
|
311
|
+
}
|
|
312
|
+
owner(id: string): string {
|
|
313
|
+
return String(this.row(id).parent_session);
|
|
314
|
+
}
|
|
315
|
+
private assertRootIdle(parent: string): void {
|
|
316
|
+
if (
|
|
317
|
+
this.store.db
|
|
318
|
+
.prepare(
|
|
319
|
+
"SELECT 1 FROM attempts WHERE session_id=? AND state IN ('accepted','dispatching','running')",
|
|
320
|
+
)
|
|
321
|
+
.get(parent) ||
|
|
322
|
+
this.store.db
|
|
323
|
+
.prepare("SELECT 1 FROM compactions WHERE session_id=? AND state='running'")
|
|
324
|
+
.get(parent)
|
|
325
|
+
)
|
|
326
|
+
throw new ServiceError('task_busy', 'Pause root work before integration');
|
|
327
|
+
}
|
|
328
|
+
private row(id: string) {
|
|
329
|
+
const row = this.store.db.prepare('SELECT * FROM worker_integrations WHERE id=?').get(id);
|
|
330
|
+
if (!row) throw new ServiceError('not_found', 'Integration receipt not found', 404);
|
|
331
|
+
return row;
|
|
332
|
+
}
|
|
333
|
+
inspect(id: string): unknown {
|
|
334
|
+
const row = this.row(id),
|
|
335
|
+
workspace = this.workspaces.get(String(row.parent_session)),
|
|
336
|
+
after = JSON.parse(this.artifacts.read(String(row.after_artifact))) as WorkspaceTree;
|
|
337
|
+
let current = false;
|
|
338
|
+
try {
|
|
339
|
+
current =
|
|
340
|
+
row.state === 'applied' &&
|
|
341
|
+
this.workspaces.verify(String(row.parent_session)).digest === digest(after);
|
|
342
|
+
} catch {}
|
|
343
|
+
return { ...row, after_digest: digest(after), current, verified_success: false };
|
|
344
|
+
}
|
|
345
|
+
reconcile(id: string): void {
|
|
346
|
+
const row = this.row(id);
|
|
347
|
+
if (row.state === 'applied') return;
|
|
348
|
+
const workspace = this.workspaces.get(String(row.parent_session)),
|
|
349
|
+
after = JSON.parse(this.artifacts.read(String(row.after_artifact))) as WorkspaceTree;
|
|
350
|
+
const mutation = this.store.db
|
|
351
|
+
.prepare('SELECT * FROM workspace_mutations WHERE workspace_id=? AND request_key=?')
|
|
352
|
+
.get(workspace.id, String(row.mutation_key));
|
|
353
|
+
const applied = mutation?.state === 'applied' && mutation.after_digest === digest(after);
|
|
354
|
+
this.store.transaction(() => {
|
|
355
|
+
this.store.db
|
|
356
|
+
.prepare('UPDATE worker_integrations SET state=?,mutation_id=? WHERE id=?')
|
|
357
|
+
.run(applied ? 'applied' : 'needs_resolution', mutation ? String(mutation.id) : null, id);
|
|
358
|
+
this.store.event(String(row.parent_session), 'worker.integration_reconciled', {
|
|
359
|
+
integration_id: id,
|
|
360
|
+
state: applied ? 'applied' : 'needs_resolution',
|
|
361
|
+
mutation_id: mutation?.id ?? null,
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
recover(): void {
|
|
366
|
+
for (const row of this.store.db
|
|
367
|
+
.prepare("SELECT id FROM worker_integrations WHERE state='prepared'")
|
|
368
|
+
.all())
|
|
369
|
+
this.reconcile(String(row.id));
|
|
370
|
+
}
|
|
371
|
+
assertStepReview(task: string, step: string): void {
|
|
372
|
+
const row = this.store.db
|
|
373
|
+
.prepare(
|
|
374
|
+
'SELECT c.id,b.binding FROM task_steps s JOIN child_tasks c ON c.child_attempt=s.attempt_id JOIN child_workers b ON b.child_id=c.id WHERE s.task_id=? AND s.id=?',
|
|
375
|
+
)
|
|
376
|
+
.get(task, step);
|
|
377
|
+
if (!row) return;
|
|
378
|
+
const binding = JSON.parse(String(row.binding)) as WorkerBinding;
|
|
379
|
+
if (!binding.tools.includes('salaam_files')) return;
|
|
380
|
+
if (
|
|
381
|
+
!this.store.db
|
|
382
|
+
.prepare("SELECT 1 FROM worker_integrations WHERE child_id=? AND state='applied'")
|
|
383
|
+
.get(String(row.id))
|
|
384
|
+
)
|
|
385
|
+
throw new ServiceError(
|
|
386
|
+
'integration_required',
|
|
387
|
+
'Integrate the exact writing-worker proposal before step verification or completion',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
package/src/workspaces.ts
CHANGED
|
@@ -293,7 +293,7 @@ export class Workspaces {
|
|
|
293
293
|
'Occurrence workspace changed before publication',
|
|
294
294
|
);
|
|
295
295
|
const baseline = this.artifacts.prepare(session, JSON.stringify(tree));
|
|
296
|
-
|
|
296
|
+
const publish = () => {
|
|
297
297
|
baseline.commit();
|
|
298
298
|
this.store.db
|
|
299
299
|
.prepare("INSERT INTO workspaces VALUES (?,?,?,?,0,?,?,'ready')")
|
|
@@ -304,7 +304,9 @@ export class Workspaces {
|
|
|
304
304
|
digest: digest(tree),
|
|
305
305
|
cloned: true,
|
|
306
306
|
});
|
|
307
|
-
}
|
|
307
|
+
};
|
|
308
|
+
if (this.store.db.isTransaction) publish();
|
|
309
|
+
else this.store.transaction(publish);
|
|
308
310
|
return this.get(session);
|
|
309
311
|
}
|
|
310
312
|
inspect(session: string): unknown {
|