maka-agent 0.2.0-dev.25.20260908 → 0.2.0-dev.26.20260909
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-capability-provider.js +4 -1
- package/dist/pi-tui-mcp-status.js +2 -2
- package/dist/pi-tui-pickers.js +19 -29
- package/dist/tui-autocomplete-layout.js +3 -1
- package/dist/tui-copy-catalog.js +6 -3
- package/dist/tui-editor-render.js +36 -0
- package/dist/tui-mcp-control.js +41 -6
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/runtime-event.js +3 -0
- package/node_modules/@maka/core/dist/session.js +29 -4
- package/node_modules/@maka/core/dist/workhub-action-result.js +68 -0
- package/node_modules/@maka/core/package.json +2 -3
- package/node_modules/@maka/eval/dist/toolchain-verification.js +1 -1
- package/node_modules/@maka/mcp/dist/form-elicitation.js +213 -0
- package/node_modules/@maka/mcp/dist/index.js +337 -71
- package/node_modules/@maka/runtime/dist/agent-run.js +10 -1
- package/node_modules/@maka/runtime/dist/goal-continuation.js +5 -1
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +20 -1
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +120 -13
- package/node_modules/@maka/runtime/dist/session-manager.js +20 -36
- package/node_modules/@maka/runtime-host/dist/client/connection.js +30 -5
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +17 -1
- package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +4 -2
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +18 -1
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +3 -2
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +68 -168
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/canonical-session-projection.js +2 -1
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +28 -8
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +112 -56
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +45 -20
- package/node_modules/@maka/runtime-host/dist/server/failure-diagnostic.js +24 -0
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +7 -3
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -3
- package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +3 -2
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +34 -4
- package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +4 -1
- package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +2 -7
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +110 -14
- package/node_modules/@maka/runtime-host/dist/server/runtime-resource-coordinator.js +11 -8
- package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +31 -21
- package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +20 -8
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +15 -104
- package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -85
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +120 -137
- package/node_modules/@maka/storage/dist/agent-run-store.js +14 -1
- package/node_modules/@maka/storage/dist/artifact-store.js +40 -3
- package/node_modules/@maka/storage/dist/atomic-file-write.js +74 -0
- package/node_modules/@maka/storage/dist/credential-store.js +12 -63
- package/node_modules/@maka/storage/dist/execution-stores.js +4 -1
- package/node_modules/@maka/storage/dist/mcp-config-store.js +10 -25
- package/node_modules/@maka/storage/dist/session-store.js +14 -2
- package/node_modules/@maka/storage/dist/settings-store.js +8 -4
- package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +11 -0
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +12 -1
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +67 -3
- package/node_modules/@maka/storage/dist/stable-storage.js +14 -1
- package/package.json +1 -1
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +0 -990
- package/node_modules/@maka/core/dist/workhub-session-resolver.js +0 -50
|
@@ -22,34 +22,35 @@ import { join } from 'node:path';
|
|
|
22
22
|
import { isDeepStrictEqual } from 'node:util';
|
|
23
23
|
import { normalizeMessageContent } from '@maka/core/events';
|
|
24
24
|
import { WORKHUB_COORDINATION_SESSION_ID, WORKHUB_COORDINATION_SESSION_ROLE, WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION, WORKHUB_COORDINATION_STOP_SCHEMA_VERSION, isWorkHubCoordinationSession, isWorkHubCoordinationSessionId, } from '@maka/core/session';
|
|
25
|
-
import {
|
|
26
|
-
import { SessionOperationFailure } from './session-catalog-coordinator.js';
|
|
25
|
+
import { WORKHUB_COORDINATION_TEXT_MAX_BYTES } from '../protocol/index.js';
|
|
26
|
+
import { SessionOperationFailure, projectSessionCatalogRecord, } from './session-catalog-coordinator.js';
|
|
27
27
|
import { WorkHubActionEffectFailure, WorkHubActionGateFailure, WorkHubCoordinationActionGate, } from './workhub-coordination-action-gate.js';
|
|
28
28
|
const CREATE_FINGERPRINT = `sha256:${createHash('sha256')
|
|
29
29
|
.update('maka:workhub-coordination-session:v1', 'utf8')
|
|
30
30
|
.digest('hex')}`;
|
|
31
31
|
const COORDINATION_CWD_DIRECTORY = 'workhub-coordination';
|
|
32
|
-
const COORDINATION_TOOL_PROFILE = 'workhub-coordination-
|
|
33
|
-
const
|
|
34
|
-
const COORDINATION_PERMISSION_MODE = 'explore';
|
|
32
|
+
const COORDINATION_TOOL_PROFILE = 'workhub-coordination-v2';
|
|
33
|
+
const COORDINATION_PERMISSION_MODE = 'bypass';
|
|
35
34
|
const COORDINATION_COLLABORATION_MODE = 'agent';
|
|
36
35
|
const COORDINATION_ORCHESTRATION_MODE = 'default';
|
|
37
36
|
const COORDINATION_SUMMARY_MESSAGE_KINDS = ['user', 'assistant', 'state'];
|
|
38
37
|
const TURN_IDENTITY_CONFLICT_MESSAGE = 'WorkHub Coordination Turn identity belongs to a different operation';
|
|
39
38
|
// A one-byte control character can occupy six bytes as a JSON `\u0000` escape.
|
|
40
39
|
const JSON_ESCAPE_MAX_BYTES_PER_INPUT_BYTE = 6;
|
|
41
|
-
const COORDINATION_SUMMARY_READ_MAX_BYTES = JSON_ESCAPE_MAX_BYTES_PER_INPUT_BYTE *
|
|
42
|
-
(WORKHUB_COORDINATION_TEXT_MAX_BYTES + WORKHUB_COORDINATION_SUMMARY_MAX_BYTES) +
|
|
40
|
+
const COORDINATION_SUMMARY_READ_MAX_BYTES = JSON_ESCAPE_MAX_BYTES_PER_INPUT_BYTE * (WORKHUB_COORDINATION_TEXT_MAX_BYTES + 8 * 1024) +
|
|
43
41
|
16 * 1024;
|
|
44
42
|
/** Resolves the one durable Coordination Session owned by this Runtime Host. */
|
|
45
43
|
export class HostWorkHubCoordinationCoordinator {
|
|
46
44
|
handlers = {
|
|
47
45
|
'workhub.coordination.resolve': () => this.#resolve(),
|
|
46
|
+
'workhub.coordination.query': () => this.#query(),
|
|
47
|
+
'workhub.coordination.configureModel': (input) => this.#configureModel(input),
|
|
48
48
|
'workhub.coordination.answer': (input, context) => this.#answer(input, context),
|
|
49
|
-
'workhub.coordination.record': (input) => this.#record(input),
|
|
50
49
|
'workhub.coordination.candidates': () => this.#candidates(),
|
|
51
|
-
'workhub.coordination.
|
|
50
|
+
'workhub.coordination.actFromTurn': (input, context) => this.#actFromTurn(input, context),
|
|
52
51
|
};
|
|
52
|
+
#transitionConfiguration;
|
|
53
|
+
#configureModel;
|
|
53
54
|
#coordinationCwd;
|
|
54
55
|
#stores;
|
|
55
56
|
#admission;
|
|
@@ -60,6 +61,8 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
60
61
|
#actionGate;
|
|
61
62
|
#readDelegationRetirement;
|
|
62
63
|
constructor(options) {
|
|
64
|
+
this.#configureModel = options.configureModel;
|
|
65
|
+
this.#transitionConfiguration = options.transitionConfiguration;
|
|
63
66
|
this.#coordinationCwd = join(options.stateRoot, COORDINATION_CWD_DIRECTORY);
|
|
64
67
|
this.#stores = options.stores;
|
|
65
68
|
this.#readDelegationRetirement = options.sessionActions.readDelegationRetirement;
|
|
@@ -87,22 +90,6 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
87
90
|
readSupersession: (delegationId) => this.#stores.readWorkHubSupersession(delegationId),
|
|
88
91
|
readStopRequest: (delegationId) => this.#stores.readWorkHubStopRequest(delegationId),
|
|
89
92
|
readStopResolution: (delegationId) => this.#stores.readWorkHubStopResolution(delegationId),
|
|
90
|
-
answer: async (input, context) => {
|
|
91
|
-
const outcome = await this.#answer(input, context);
|
|
92
|
-
if (!outcome.ok) {
|
|
93
|
-
throw new WorkHubActionEffectFailure(outcome.error.code, outcome.error.message);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
clarify: async (input) => {
|
|
97
|
-
const outcome = await this.#record({
|
|
98
|
-
turnId: input.turnId,
|
|
99
|
-
userText: input.userText,
|
|
100
|
-
assistantText: input.assistantText,
|
|
101
|
-
});
|
|
102
|
-
if (!outcome.ok) {
|
|
103
|
-
throw new WorkHubActionEffectFailure(outcome.error.code, outcome.error.message);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
93
|
assign: options.sessionActions.assign,
|
|
107
94
|
prepareReplacement: (input) => this.#prepareReplacement(input),
|
|
108
95
|
abortReplacement: (input) => this.#abortReplacement(input),
|
|
@@ -113,7 +100,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
113
100
|
resume: async (input, context) => ({
|
|
114
101
|
disposition: 'resume_work',
|
|
115
102
|
targetSessionId: input.source.targetSessionId,
|
|
116
|
-
...(await options.sessionActions.resumeDelegation(input.source, context, input.actionId)),
|
|
103
|
+
...(await options.sessionActions.resumeDelegation(input.source, context, input.actionId, input.validateFreshTarget)),
|
|
117
104
|
}),
|
|
118
105
|
});
|
|
119
106
|
}
|
|
@@ -125,18 +112,19 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
125
112
|
build: (existing) => ({
|
|
126
113
|
type: 'workhub_coordination',
|
|
127
114
|
id: `whp_${suffix}`,
|
|
128
|
-
turnId: input.actionId,
|
|
115
|
+
turnId: existing?.turnId ?? input.coordinationTurnId ?? input.actionId,
|
|
129
116
|
ts: existing?.ts ?? Date.now(),
|
|
130
117
|
schemaVersion: WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION,
|
|
131
118
|
kind: 'delegation_replacement_requested',
|
|
132
119
|
actionId: input.actionId,
|
|
133
120
|
actionFingerprint: input.actionFingerprint,
|
|
134
|
-
coordinationTurnId: input.actionId,
|
|
121
|
+
coordinationTurnId: existing?.coordinationTurnId ?? input.coordinationTurnId ?? input.actionId,
|
|
135
122
|
targetSessionId: input.targetSessionId,
|
|
136
123
|
targetSessionName: input.targetSessionName,
|
|
137
124
|
disposition: input.disposition,
|
|
138
125
|
userText: input.userText,
|
|
139
126
|
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
127
|
+
...(input.delegationText === undefined ? {} : { delegationText: input.delegationText }),
|
|
140
128
|
replacesActionId: input.replacesActionId,
|
|
141
129
|
replacesDelegationId: input.replacesDelegationId,
|
|
142
130
|
replacedTargetSessionId: input.replacedTargetSessionId,
|
|
@@ -178,13 +166,13 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
178
166
|
build: (existing) => ({
|
|
179
167
|
type: 'workhub_coordination',
|
|
180
168
|
id: `whq_${suffix}`,
|
|
181
|
-
turnId: input.actionId,
|
|
169
|
+
turnId: existing?.turnId ?? input.coordinationTurnId ?? input.actionId,
|
|
182
170
|
ts: existing?.ts ?? Date.now(),
|
|
183
171
|
schemaVersion: WORKHUB_COORDINATION_STOP_SCHEMA_VERSION,
|
|
184
172
|
kind: 'delegation_stop_requested',
|
|
185
173
|
actionId: input.actionId,
|
|
186
174
|
actionFingerprint: input.actionFingerprint,
|
|
187
|
-
coordinationTurnId: input.actionId,
|
|
175
|
+
coordinationTurnId: existing?.coordinationTurnId ?? input.coordinationTurnId ?? input.actionId,
|
|
188
176
|
stopsActionId: input.stopsActionId,
|
|
189
177
|
stopsDelegationId: input.stopsDelegationId,
|
|
190
178
|
targetSessionId: input.targetSessionId,
|
|
@@ -232,7 +220,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
232
220
|
build: (existing) => ({
|
|
233
221
|
type: 'workhub_coordination',
|
|
234
222
|
id: `whz_${suffix}`,
|
|
235
|
-
turnId: request.
|
|
223
|
+
turnId: request.coordinationTurnId,
|
|
236
224
|
ts: existing?.ts ?? Date.now(),
|
|
237
225
|
schemaVersion: WORKHUB_COORDINATION_STOP_SCHEMA_VERSION,
|
|
238
226
|
kind: 'delegation_stop_resolved',
|
|
@@ -263,13 +251,13 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
263
251
|
build: (existing) => ({
|
|
264
252
|
type: 'workhub_coordination',
|
|
265
253
|
id: `whb_${suffix}`,
|
|
266
|
-
turnId: replacement.
|
|
254
|
+
turnId: replacement.coordinationTurnId,
|
|
267
255
|
ts: existing?.ts ?? Date.now(),
|
|
268
256
|
schemaVersion: WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION,
|
|
269
257
|
kind: 'delegation_replacement_aborted',
|
|
270
258
|
actionId: replacement.actionId,
|
|
271
259
|
actionFingerprint: replacement.actionFingerprint,
|
|
272
|
-
coordinationTurnId: replacement.
|
|
260
|
+
coordinationTurnId: replacement.coordinationTurnId,
|
|
273
261
|
abortedActionId: replacement.replacesActionId,
|
|
274
262
|
abortedDelegationId: replacement.replacesDelegationId,
|
|
275
263
|
targetSessionId: replacement.targetSessionId,
|
|
@@ -339,9 +327,44 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
339
327
|
};
|
|
340
328
|
}
|
|
341
329
|
}
|
|
342
|
-
async #
|
|
330
|
+
async #actFromTurn(input, context) {
|
|
331
|
+
let content;
|
|
343
332
|
try {
|
|
344
|
-
|
|
333
|
+
content = await this.#executions.readActiveWorkHubRequest(input.turnId);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
error: {
|
|
339
|
+
code: 'persistence_failed',
|
|
340
|
+
message: 'WorkHub active Turn authority is unavailable',
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (!content) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
error: {
|
|
348
|
+
code: 'operation_conflict',
|
|
349
|
+
message: 'WorkHub action requires its currently active model Turn',
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const { turnId: _turnId, ...action } = input;
|
|
354
|
+
const carriesAttachments = action.proposal.disposition === 'delegate_existing' ||
|
|
355
|
+
action.proposal.disposition === 'create_new' ||
|
|
356
|
+
action.proposal.disposition === 'replace';
|
|
357
|
+
try {
|
|
358
|
+
return {
|
|
359
|
+
ok: true,
|
|
360
|
+
result: await this.#actionGate.act({
|
|
361
|
+
...action,
|
|
362
|
+
userText: content.text,
|
|
363
|
+
...(carriesAttachments && content.attachments
|
|
364
|
+
? { attachments: content.attachments }
|
|
365
|
+
: {}),
|
|
366
|
+
}, context, input.turnId),
|
|
367
|
+
};
|
|
345
368
|
}
|
|
346
369
|
catch (error) {
|
|
347
370
|
if (error instanceof WorkHubActionEffectFailure) {
|
|
@@ -357,7 +380,11 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
357
380
|
return {
|
|
358
381
|
ok: false,
|
|
359
382
|
error: {
|
|
360
|
-
code: error.code === 'target_waiting_for_user'
|
|
383
|
+
code: error.code === 'target_waiting_for_user'
|
|
384
|
+
? 'session_busy'
|
|
385
|
+
: error.code === 'candidate_set_stale'
|
|
386
|
+
? 'candidate_set_stale'
|
|
387
|
+
: 'operation_conflict',
|
|
361
388
|
message: error.message,
|
|
362
389
|
},
|
|
363
390
|
};
|
|
@@ -371,6 +398,23 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
371
398
|
};
|
|
372
399
|
}
|
|
373
400
|
}
|
|
401
|
+
async #query() {
|
|
402
|
+
try {
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
result: projectSessionCatalogRecord(await this.#stores.readCatalogRecord(WORKHUB_COORDINATION_SESSION_ID, 'recoverable')),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
error: {
|
|
412
|
+
code: 'persistence_failed',
|
|
413
|
+
message: 'WorkHub Coordination Session state is unavailable',
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
374
418
|
#resolve() {
|
|
375
419
|
return this.#admission.run(WORKHUB_COORDINATION_SESSION_ID, async (lease) => {
|
|
376
420
|
// The workspace exists for provisioning and for reuse alike: a directory
|
|
@@ -437,7 +481,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
437
481
|
}
|
|
438
482
|
async #answer(input, context) {
|
|
439
483
|
if (!input.text.trim()) {
|
|
440
|
-
return turnFailure('
|
|
484
|
+
return turnFailure('operation_conflict', 'WorkHub answer text is empty');
|
|
441
485
|
}
|
|
442
486
|
const outcome = await this.#executions.startWorkHubCoordinationMessage({
|
|
443
487
|
sessionId: WORKHUB_COORDINATION_SESSION_ID,
|
|
@@ -450,9 +494,8 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
450
494
|
}),
|
|
451
495
|
},
|
|
452
496
|
archivedMessage: 'WorkHub Coordination Session is unavailable',
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
// concurrent `record` cannot slip a second triplet into this Turn.
|
|
497
|
+
// Historical v1 summaries still own their Turn identities. Reject a
|
|
498
|
+
// fresh answer that would reuse one, even though new summaries are no longer written.
|
|
456
499
|
prepareFreshContent: async () => {
|
|
457
500
|
let recorded;
|
|
458
501
|
try {
|
|
@@ -477,46 +520,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
477
520
|
}, context);
|
|
478
521
|
return outcome.ok ? { ok: true, result: { turnId: input.turnId } } : outcome;
|
|
479
522
|
}
|
|
480
|
-
|
|
481
|
-
if (!input.userText.trim() || !input.assistantText.trim()) {
|
|
482
|
-
return Promise.resolve(turnFailure('workhub.coordination.record', 'operation_conflict', 'WorkHub Coordination summary text is empty'));
|
|
483
|
-
}
|
|
484
|
-
return this.#admission.run(WORKHUB_COORDINATION_SESSION_ID, async (lease) => {
|
|
485
|
-
let header;
|
|
486
|
-
try {
|
|
487
|
-
header = await this.#stores.readHeaderSnapshot(WORKHUB_COORDINATION_SESSION_ID);
|
|
488
|
-
}
|
|
489
|
-
catch {
|
|
490
|
-
return turnFailure('workhub.coordination.record', 'persistence_failed', 'WorkHub Coordination Session state is unavailable');
|
|
491
|
-
}
|
|
492
|
-
if (!validCoordinationHeader(header)) {
|
|
493
|
-
return turnFailure('workhub.coordination.record', 'operation_conflict', 'WorkHub Coordination Session identity is unavailable');
|
|
494
|
-
}
|
|
495
|
-
const messages = coordinationSummaryMessages(input);
|
|
496
|
-
try {
|
|
497
|
-
const existing = await this.#readSummaryMessages(input.turnId);
|
|
498
|
-
if (existing.length > 0) {
|
|
499
|
-
return coordinationSummaryMatches(existing, input)
|
|
500
|
-
? { ok: true, result: { turnId: input.turnId } }
|
|
501
|
-
: turnFailure('workhub.coordination.record', 'operation_conflict', 'WorkHub Coordination Turn identity belongs to different content');
|
|
502
|
-
}
|
|
503
|
-
// An answer owns its Turn identity in the root admission ledger. Both
|
|
504
|
-
// operations take the same Session admission, so this probe settles the
|
|
505
|
-
// race in one direction and the answer's own probe settles the other.
|
|
506
|
-
if (await this.#executions.hasRootTurnAdmission(WORKHUB_COORDINATION_SESSION_ID, input.turnId)) {
|
|
507
|
-
return turnFailure('workhub.coordination.record', 'operation_conflict', TURN_IDENTITY_CONFLICT_MESSAGE);
|
|
508
|
-
}
|
|
509
|
-
await this.#stores.appendMessages(WORKHUB_COORDINATION_SESSION_ID, messages);
|
|
510
|
-
await this.#continuity.refreshCanonical(WORKHUB_COORDINATION_SESSION_ID, lease);
|
|
511
|
-
return { ok: true, result: { turnId: input.turnId } };
|
|
512
|
-
}
|
|
513
|
-
catch {
|
|
514
|
-
this.#requestDrain();
|
|
515
|
-
return turnFailure('workhub.coordination.record', 'commit_outcome_unknown', 'WorkHub Coordination summary outcome is unknown');
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
/** Reads the durable summary triplet a `record` would own for this Turn. */
|
|
523
|
+
/** Reads a historical v1 summary to preserve its durable Turn identity. */
|
|
520
524
|
async #readSummaryMessages(turnId) {
|
|
521
525
|
const throughSequence = await this.#stores.readTranscriptHighWaterSnapshot(WORKHUB_COORDINATION_SESSION_ID);
|
|
522
526
|
if (throughSequence === null)
|
|
@@ -536,32 +540,52 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
536
540
|
*/
|
|
537
541
|
async #alignSession(record) {
|
|
538
542
|
if (record.header.toolProfile !== undefined &&
|
|
543
|
+
record.header.toolProfile !== 'workhub-coordination-v1' &&
|
|
539
544
|
record.header.toolProfile !== COORDINATION_TOOL_PROFILE) {
|
|
540
545
|
return identityConflict();
|
|
541
546
|
}
|
|
542
|
-
if (record.header.cwd === this.#coordinationCwd &&
|
|
547
|
+
if (record.header.cwd === this.#coordinationCwd &&
|
|
548
|
+
record.header.toolProfile === COORDINATION_TOOL_PROFILE &&
|
|
549
|
+
validCoordinationHeader(record.header)) {
|
|
543
550
|
return success();
|
|
544
551
|
}
|
|
552
|
+
// Resolving an old Session is the explicit durable upgrade boundary. Active
|
|
553
|
+
// or recovering v1 Turns must finish under their original zero-tool ceiling.
|
|
554
|
+
if (!this.#executions.isSessionExecutionIdle(WORKHUB_COORDINATION_SESSION_ID)) {
|
|
555
|
+
return validCoordinationHeader(record.header)
|
|
556
|
+
? success()
|
|
557
|
+
: failure('operation_unavailable', 'WorkHub Coordination Session is still executing or recovering');
|
|
558
|
+
}
|
|
545
559
|
let repaired;
|
|
546
560
|
try {
|
|
561
|
+
// Permission mode is a projection of the execution boundary. Transition
|
|
562
|
+
// through Runtime authority to update both and retire cached backends
|
|
563
|
+
// before changing the profile. An interrupted upgrade remains closed to
|
|
564
|
+
// execution until a later resolve completes this versioned repair.
|
|
565
|
+
const configured = await this.#transitionConfiguration({
|
|
566
|
+
expectedRevision: record.revision,
|
|
567
|
+
clearConnectionBlock: false,
|
|
568
|
+
configuration: {
|
|
569
|
+
backend: record.header.backend,
|
|
570
|
+
llmConnectionId: record.header.llmConnectionId,
|
|
571
|
+
llmConnectionSlug: record.header.llmConnectionSlug,
|
|
572
|
+
connectionLocked: record.header.connectionLocked,
|
|
573
|
+
model: record.header.model,
|
|
574
|
+
thinkingLevel: record.header.thinkingLevel,
|
|
575
|
+
permissionMode: COORDINATION_PERMISSION_MODE,
|
|
576
|
+
collaborationMode: COORDINATION_COLLABORATION_MODE,
|
|
577
|
+
orchestrationMode: COORDINATION_ORCHESTRATION_MODE,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
547
580
|
repaired = await this.#stores.updateHeaderVersioned(WORKHUB_COORDINATION_SESSION_ID, {
|
|
548
581
|
...(record.header.cwd === this.#coordinationCwd ? {} : { cwd: this.#coordinationCwd }),
|
|
549
582
|
...(record.header.toolProfile === COORDINATION_TOOL_PROFILE
|
|
550
583
|
? {}
|
|
551
584
|
: { toolProfile: COORDINATION_TOOL_PROFILE }),
|
|
552
|
-
|
|
553
|
-
? {}
|
|
554
|
-
: { permissionMode: COORDINATION_PERMISSION_MODE }),
|
|
555
|
-
...((record.header.collaborationMode ?? 'agent') === COORDINATION_COLLABORATION_MODE
|
|
556
|
-
? {}
|
|
557
|
-
: { collaborationMode: COORDINATION_COLLABORATION_MODE }),
|
|
558
|
-
...((record.header.orchestrationMode ?? 'default') === COORDINATION_ORCHESTRATION_MODE
|
|
559
|
-
? {}
|
|
560
|
-
: { orchestrationMode: COORDINATION_ORCHESTRATION_MODE }),
|
|
561
|
-
}, record.revision);
|
|
585
|
+
}, configured.revision);
|
|
562
586
|
}
|
|
563
587
|
catch {
|
|
564
|
-
return failure('persistence_failed', 'WorkHub Coordination Session
|
|
588
|
+
return failure('persistence_failed', 'WorkHub Coordination Session configuration could not be aligned');
|
|
565
589
|
}
|
|
566
590
|
return validCoordinationHeader(repaired.header) && repaired.header.cwd === this.#coordinationCwd
|
|
567
591
|
? success()
|
|
@@ -587,8 +611,9 @@ function validCoordinationIdentityHeader(header) {
|
|
|
587
611
|
}
|
|
588
612
|
function validCoordinationHeader(header) {
|
|
589
613
|
return (validCoordinationIdentityHeader(header) &&
|
|
590
|
-
header.toolProfile === COORDINATION_TOOL_PROFILE &&
|
|
591
|
-
|
|
614
|
+
((header.toolProfile === COORDINATION_TOOL_PROFILE &&
|
|
615
|
+
header.permissionMode === COORDINATION_PERMISSION_MODE) ||
|
|
616
|
+
(header.toolProfile === 'workhub-coordination-v1' && header.permissionMode === 'explore')) &&
|
|
592
617
|
(header.collaborationMode ?? 'agent') === COORDINATION_COLLABORATION_MODE &&
|
|
593
618
|
(header.orchestrationMode ?? 'default') === COORDINATION_ORCHESTRATION_MODE);
|
|
594
619
|
}
|
|
@@ -604,48 +629,6 @@ function coordinationSummaryMessageId(turnId, kind) {
|
|
|
604
629
|
.digest('hex')
|
|
605
630
|
.slice(0, 48)}`;
|
|
606
631
|
}
|
|
607
|
-
function coordinationSummaryMessages(input) {
|
|
608
|
-
const ts = Date.now();
|
|
609
|
-
const messageId = (kind) => coordinationSummaryMessageId(input.turnId, kind);
|
|
610
|
-
return [
|
|
611
|
-
{
|
|
612
|
-
type: 'user',
|
|
613
|
-
id: messageId('user'),
|
|
614
|
-
turnId: input.turnId,
|
|
615
|
-
ts,
|
|
616
|
-
text: input.userText,
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
type: 'assistant',
|
|
620
|
-
id: messageId('assistant'),
|
|
621
|
-
turnId: input.turnId,
|
|
622
|
-
ts: ts + 1,
|
|
623
|
-
text: input.assistantText,
|
|
624
|
-
modelId: SYNTHETIC_COORDINATION_MODEL_ID,
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
type: 'turn_state',
|
|
628
|
-
id: messageId('state'),
|
|
629
|
-
turnId: input.turnId,
|
|
630
|
-
ts: ts + 2,
|
|
631
|
-
status: 'completed',
|
|
632
|
-
},
|
|
633
|
-
];
|
|
634
|
-
}
|
|
635
|
-
function coordinationSummaryMatches(existing, input) {
|
|
636
|
-
if (existing.length !== 3)
|
|
637
|
-
return false;
|
|
638
|
-
const user = existing.find((message) => message.type === 'user');
|
|
639
|
-
const assistant = existing.find((message) => message.type === 'assistant');
|
|
640
|
-
const state = existing.find((message) => message.type === 'turn_state');
|
|
641
|
-
return (user?.turnId === input.turnId &&
|
|
642
|
-
user.text === input.userText &&
|
|
643
|
-
assistant?.turnId === input.turnId &&
|
|
644
|
-
assistant.text === input.assistantText &&
|
|
645
|
-
assistant.modelId === SYNTHETIC_COORDINATION_MODEL_ID &&
|
|
646
|
-
state?.turnId === input.turnId &&
|
|
647
|
-
state.status === 'completed');
|
|
648
|
-
}
|
|
649
632
|
function success() {
|
|
650
633
|
return {
|
|
651
634
|
ok: true,
|
|
@@ -668,6 +651,6 @@ function turnIdentityConflict() {
|
|
|
668
651
|
function operationUnavailable(message) {
|
|
669
652
|
return { ok: false, error: { code: 'operation_unavailable', message } };
|
|
670
653
|
}
|
|
671
|
-
function turnFailure(
|
|
654
|
+
function turnFailure(code, message) {
|
|
672
655
|
return { ok: false, error: { code, message } };
|
|
673
656
|
}
|
|
@@ -1347,12 +1347,25 @@ function normalizeRootExecutionDescriptor(value) {
|
|
|
1347
1347
|
});
|
|
1348
1348
|
}
|
|
1349
1349
|
if (value.kind === 'workhub_coordination') {
|
|
1350
|
-
if (!hasExactKeys(value,
|
|
1350
|
+
if (!hasExactKeys(value, value.operation === undefined
|
|
1351
|
+
? ['kind', 'inputDigest']
|
|
1352
|
+
: [
|
|
1353
|
+
'kind',
|
|
1354
|
+
'inputDigest',
|
|
1355
|
+
'operation',
|
|
1356
|
+
...(value.actionId === undefined ? [] : ['actionId']),
|
|
1357
|
+
]) ||
|
|
1358
|
+
(value.operation !== undefined && value.operation !== 'action') ||
|
|
1359
|
+
(value.actionId !== undefined &&
|
|
1360
|
+
(typeof value.actionId !== 'string' || !isSafeId(value.actionId))) ||
|
|
1361
|
+
!isSha256Digest(value.inputDigest)) {
|
|
1351
1362
|
throw new Error('Invalid root execution descriptor');
|
|
1352
1363
|
}
|
|
1353
1364
|
return Object.freeze({
|
|
1354
1365
|
kind: 'workhub_coordination',
|
|
1366
|
+
...(value.operation === 'action' ? { operation: 'action' } : {}),
|
|
1355
1367
|
inputDigest: value.inputDigest,
|
|
1368
|
+
...(typeof value.actionId === 'string' ? { actionId: value.actionId } : {}),
|
|
1356
1369
|
});
|
|
1357
1370
|
}
|
|
1358
1371
|
if (value.kind === 'regenerate') {
|
|
@@ -252,6 +252,7 @@ class SqliteArtifactStore {
|
|
|
252
252
|
const directories = new Set();
|
|
253
253
|
const discharged = [];
|
|
254
254
|
let failedPaths = 0;
|
|
255
|
+
let realArtifactRoot;
|
|
255
256
|
try {
|
|
256
257
|
for (const relativePath of selected) {
|
|
257
258
|
if (this.metadataRepository.hasRelativePath(relativePath) ||
|
|
@@ -259,10 +260,28 @@ class SqliteArtifactStore {
|
|
|
259
260
|
discharged.push(relativePath);
|
|
260
261
|
continue;
|
|
261
262
|
}
|
|
262
|
-
const
|
|
263
|
+
const entry = await resolveArtifactRemovalEntry(this.artifactRoot, relativePath);
|
|
264
|
+
if (!entry) {
|
|
265
|
+
discharged.push(relativePath);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
realArtifactRoot ??= await ensureRealDirectory(this.artifactRoot);
|
|
269
|
+
if (!isInsideOrSamePath(realArtifactRoot, dirname(entry.unlinkPath))) {
|
|
270
|
+
failedPaths += 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const artifactIds = new Set([
|
|
274
|
+
...artifactIdsFromUpgradeOrphanPath(relativePath),
|
|
275
|
+
...artifactIdsFromUpgradeOrphanPath(entry.unlinkPath),
|
|
276
|
+
]);
|
|
277
|
+
if (artifactIds.size > 0 &&
|
|
278
|
+
(await this.hasClaimedRemovalIdentityUnlocked([...artifactIds], entry.comparisonIdentity))) {
|
|
279
|
+
discharged.push(relativePath);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
263
282
|
try {
|
|
264
|
-
await unlink(
|
|
265
|
-
directories.add(dirname(
|
|
283
|
+
await unlink(entry.unlinkPath);
|
|
284
|
+
directories.add(dirname(entry.unlinkPath));
|
|
266
285
|
}
|
|
267
286
|
catch (error) {
|
|
268
287
|
if (!isNotFound(error)) {
|
|
@@ -286,6 +305,14 @@ class SqliteArtifactStore {
|
|
|
286
305
|
};
|
|
287
306
|
});
|
|
288
307
|
}
|
|
308
|
+
async hasClaimedRemovalIdentityUnlocked(artifactIds, comparisonIdentity) {
|
|
309
|
+
for (const relativePath of this.metadataRepository.readRelativePathsByCaseFoldedArtifactIds(artifactIds)) {
|
|
310
|
+
const entry = await resolveArtifactRemovalEntry(this.artifactRoot, relativePath);
|
|
311
|
+
if (entry?.comparisonIdentity === comparisonIdentity)
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
289
316
|
async replayExistingArtifactUnlocked(existing, input, canonical) {
|
|
290
317
|
const expectedBytes = Buffer.from(input.content);
|
|
291
318
|
if (existing.id !== canonical.id ||
|
|
@@ -835,6 +862,16 @@ function symlinkEntryIdentity(entryStat) {
|
|
|
835
862
|
entryStat.mtimeNs,
|
|
836
863
|
].join(':');
|
|
837
864
|
}
|
|
865
|
+
function artifactIdsFromUpgradeOrphanPath(relativePath) {
|
|
866
|
+
const name = basename(relativePath);
|
|
867
|
+
const artifactIds = [];
|
|
868
|
+
for (let separator = name.indexOf('-'); separator > 0; separator = name.indexOf('-', separator + 1)) {
|
|
869
|
+
const artifactId = name.slice(0, separator);
|
|
870
|
+
if (isCanonicalArtifactEntityId(artifactId))
|
|
871
|
+
artifactIds.push(artifactId);
|
|
872
|
+
}
|
|
873
|
+
return artifactIds;
|
|
874
|
+
}
|
|
838
875
|
function isInsideOrSamePath(root, target) {
|
|
839
876
|
if (target === root)
|
|
840
877
|
return true;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { open, rename, rm } from 'node:fs/promises';
|
|
21
|
+
import { basename, dirname, join } from 'node:path';
|
|
22
|
+
import { syncDirectory } from './stable-storage.js';
|
|
23
|
+
const defaultDependencies = {
|
|
24
|
+
open,
|
|
25
|
+
randomUUID,
|
|
26
|
+
syncDirectory,
|
|
27
|
+
};
|
|
28
|
+
export class AtomicFileWriteCommitUnknownError extends Error {
|
|
29
|
+
published = true;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
super('Atomic file commit outcome is unknown; reload before retrying', options);
|
|
32
|
+
this.name = 'AtomicFileWriteCommitUnknownError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function writeAtomicFile(path, contents, options, dependencies = {}) {
|
|
36
|
+
const deps = { ...defaultDependencies, ...dependencies };
|
|
37
|
+
const { fileMode } = options;
|
|
38
|
+
const tempPath = join(dirname(path), `.${basename(path)}.${deps.randomUUID()}.tmp`);
|
|
39
|
+
// Only the entry this call created — and hasn't renamed away — may be
|
|
40
|
+
// cleaned up. A pre-existing file or planted symlink the 'wx' open refused
|
|
41
|
+
// must not be deleted as "our" temp.
|
|
42
|
+
let tempCreated = false;
|
|
43
|
+
let published = false;
|
|
44
|
+
try {
|
|
45
|
+
const handle = await deps.open(tempPath, 'wx', fileMode);
|
|
46
|
+
tempCreated = true;
|
|
47
|
+
try {
|
|
48
|
+
await handle.writeFile(contents, 'utf8');
|
|
49
|
+
if (process.platform !== 'win32')
|
|
50
|
+
await handle.chmod(fileMode);
|
|
51
|
+
await handle.sync();
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// Release the descriptor best-effort; never let a close failure mask
|
|
56
|
+
// the error that actually aborted the write.
|
|
57
|
+
await handle.close().catch(() => { });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
await rename(tempPath, path);
|
|
61
|
+
published = true;
|
|
62
|
+
tempCreated = false;
|
|
63
|
+
await deps.syncDirectory(dirname(path));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (tempCreated) {
|
|
67
|
+
// Cleanup must never mask the original failure.
|
|
68
|
+
await rm(tempPath, { force: true }).catch(() => { });
|
|
69
|
+
}
|
|
70
|
+
if (published)
|
|
71
|
+
throw new AtomicFileWriteCommitUnknownError({ cause: error });
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|