maka-agent 0.2.0-dev.24.20260907 → 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/README.md +38 -19
- package/README.zh-CN.md +34 -19
- package/dist/mcp-capability-provider.js +4 -1
- package/dist/pi-transcript-tools.js +6 -0
- package/dist/pi-tui-layout.js +56 -10
- package/dist/pi-tui-mcp-status.js +2 -2
- package/dist/pi-tui-pickers.js +121 -30
- package/dist/pi-tui-runner.js +156 -42
- package/dist/pi-tui-todo.js +221 -0
- package/dist/pi-tui-transcript-viewer.js +134 -7
- package/dist/runtime-host-cli-context.js +70 -13
- package/dist/runtime-host-handoff-surface.js +51 -26
- package/dist/runtime-host-local-handoff.js +11 -1
- package/dist/runtime-host-service-manager.js +0 -3
- package/dist/runtime-host-session-channel.js +13 -0
- package/dist/runtime-host-session-driver.js +42 -1
- package/dist/tui-autocomplete-layout.js +3 -1
- package/dist/tui-copy-catalog.js +85 -6
- 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/agent-run.js +1 -0
- package/node_modules/@maka/core/dist/attachments.js +3 -0
- package/node_modules/@maka/core/dist/canonical-json.js +27 -0
- package/node_modules/@maka/core/dist/capabilities.js +35 -0
- package/node_modules/@maka/core/dist/computer-use.js +0 -1
- package/node_modules/@maka/core/dist/display-redaction.js +18 -0
- package/node_modules/@maka/core/dist/llm-connections.js +13 -0
- package/node_modules/@maka/core/dist/model-call-attempt.js +2 -0
- package/node_modules/@maka/core/dist/model-metadata.js +4 -0
- package/node_modules/@maka/core/dist/redaction.js +35 -0
- package/node_modules/@maka/core/dist/run-composition.js +77 -2
- package/node_modules/@maka/core/dist/runtime-event.js +3 -0
- package/node_modules/@maka/core/dist/session.js +57 -6
- package/node_modules/@maka/core/dist/slash-command-catalog.js +1 -0
- package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
- package/node_modules/@maka/core/dist/workhub-action-result.js +68 -0
- package/node_modules/@maka/core/package.json +3 -3
- package/node_modules/@maka/eval/dist/experiment-directory.js +4 -13
- 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-inspect.js +0 -13
- package/node_modules/@maka/runtime/dist/agent-run.js +90 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +16 -8
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +57 -50
- package/node_modules/@maka/runtime/dist/archive-read-tool.js +2 -2
- package/node_modules/@maka/runtime/dist/context-budget-helpers.js +0 -16
- package/node_modules/@maka/runtime/dist/conversation-copy.js +153 -11
- package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +3 -3
- package/node_modules/@maka/runtime/dist/goal-continuation.js +5 -1
- package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +69 -1
- package/node_modules/@maka/runtime/dist/mcp-tools.js +7 -2
- package/node_modules/@maka/runtime/dist/model-fetcher.js +42 -2
- package/node_modules/@maka/runtime/dist/model-history.js +0 -112
- package/node_modules/@maka/runtime/dist/plugin-runtime.js +20 -10
- package/node_modules/@maka/runtime/dist/plugin-tool-service.js +205 -0
- package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +8 -1
- package/node_modules/@maka/runtime/dist/request-shape.js +23 -0
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -1
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +24 -1
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +127 -13
- package/node_modules/@maka/runtime/dist/session-manager.js +20 -36
- package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +21 -0
- package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +0 -5
- package/node_modules/@maka/runtime/dist/test-connection.js +0 -1
- package/node_modules/@maka/runtime/dist/tool-activation-identity.js +41 -0
- package/node_modules/@maka/runtime/dist/tool-availability.js +11 -1
- package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +63 -1
- package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +42 -12
- package/node_modules/@maka/runtime/dist/tool-result-archive.js +27 -4
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/client/connection.js +30 -5
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +52 -13
- package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +2 -0
- 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/client/session-subscription.js +19 -0
- package/node_modules/@maka/runtime-host/dist/control/endpoint.js +1 -3
- package/node_modules/@maka/runtime-host/dist/node-error.js +22 -0
- package/node_modules/@maka/runtime-host/dist/operator/managed-deployment.js +1 -3
- package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +0 -1
- package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +1 -1
- package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +1 -3
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +3 -2
- package/node_modules/@maka/runtime-host/dist/protocol/plan.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +27 -5
- package/node_modules/@maka/runtime-host/dist/protocol/project-catalog.js +2 -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 +86 -157
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/agent-graph-coordinator.js +0 -3
- 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-artifacts.js +32 -32
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +85 -25
- package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +42 -14
- 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 +51 -23
- package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +2 -7
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
- 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-retirement-coordinator.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
- 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 +59 -82
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +132 -139
- package/node_modules/@maka/runtime-host/dist/server/workhub-message-attachments.js +46 -0
- 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/claude-code-session-adapter.js +0 -1
- package/node_modules/@maka/storage/dist/codex-session-adapter.js +284 -196
- 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/git-exec.js +61 -0
- package/node_modules/@maka/storage/dist/git-worktree-child-executor.js +8 -35
- package/node_modules/@maka/storage/dist/goal-authority.js +2 -2
- package/node_modules/@maka/storage/dist/interaction-store.js +2 -2
- package/node_modules/@maka/storage/dist/mcp-config-store.js +10 -25
- package/node_modules/@maka/storage/dist/project-catalog.js +2 -23
- package/node_modules/@maka/storage/dist/runtime-event-invariants.js +2 -2
- package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +8 -3
- package/node_modules/@maka/storage/dist/scheduled-task-store.js +249 -283
- package/node_modules/@maka/storage/dist/session-bundle-manifest.js +5 -17
- package/node_modules/@maka/storage/dist/session-bundle-policy.js +2 -1
- package/node_modules/@maka/storage/dist/session-copy-cleanup.js +2 -1
- 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-runtime-store.js +1 -2
- 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/node_modules/@maka/storage/dist/storage-id.js +28 -0
- package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +31 -4
- package/node_modules/@maka/storage/dist/workspace-identity.js +2 -15
- package/node_modules/@maka/storage/dist/workspace-version-authority-internal.js +1 -8
- 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({ turnId: input.turnId, text: input.text }, 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,17 +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,
|
|
126
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
127
|
+
...(input.delegationText === undefined ? {} : { delegationText: input.delegationText }),
|
|
139
128
|
replacesActionId: input.replacesActionId,
|
|
140
129
|
replacesDelegationId: input.replacesDelegationId,
|
|
141
130
|
replacedTargetSessionId: input.replacedTargetSessionId,
|
|
@@ -177,13 +166,13 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
177
166
|
build: (existing) => ({
|
|
178
167
|
type: 'workhub_coordination',
|
|
179
168
|
id: `whq_${suffix}`,
|
|
180
|
-
turnId: input.actionId,
|
|
169
|
+
turnId: existing?.turnId ?? input.coordinationTurnId ?? input.actionId,
|
|
181
170
|
ts: existing?.ts ?? Date.now(),
|
|
182
171
|
schemaVersion: WORKHUB_COORDINATION_STOP_SCHEMA_VERSION,
|
|
183
172
|
kind: 'delegation_stop_requested',
|
|
184
173
|
actionId: input.actionId,
|
|
185
174
|
actionFingerprint: input.actionFingerprint,
|
|
186
|
-
coordinationTurnId: input.actionId,
|
|
175
|
+
coordinationTurnId: existing?.coordinationTurnId ?? input.coordinationTurnId ?? input.actionId,
|
|
187
176
|
stopsActionId: input.stopsActionId,
|
|
188
177
|
stopsDelegationId: input.stopsDelegationId,
|
|
189
178
|
targetSessionId: input.targetSessionId,
|
|
@@ -231,7 +220,7 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
231
220
|
build: (existing) => ({
|
|
232
221
|
type: 'workhub_coordination',
|
|
233
222
|
id: `whz_${suffix}`,
|
|
234
|
-
turnId: request.
|
|
223
|
+
turnId: request.coordinationTurnId,
|
|
235
224
|
ts: existing?.ts ?? Date.now(),
|
|
236
225
|
schemaVersion: WORKHUB_COORDINATION_STOP_SCHEMA_VERSION,
|
|
237
226
|
kind: 'delegation_stop_resolved',
|
|
@@ -262,13 +251,13 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
262
251
|
build: (existing) => ({
|
|
263
252
|
type: 'workhub_coordination',
|
|
264
253
|
id: `whb_${suffix}`,
|
|
265
|
-
turnId: replacement.
|
|
254
|
+
turnId: replacement.coordinationTurnId,
|
|
266
255
|
ts: existing?.ts ?? Date.now(),
|
|
267
256
|
schemaVersion: WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION,
|
|
268
257
|
kind: 'delegation_replacement_aborted',
|
|
269
258
|
actionId: replacement.actionId,
|
|
270
259
|
actionFingerprint: replacement.actionFingerprint,
|
|
271
|
-
coordinationTurnId: replacement.
|
|
260
|
+
coordinationTurnId: replacement.coordinationTurnId,
|
|
272
261
|
abortedActionId: replacement.replacesActionId,
|
|
273
262
|
abortedDelegationId: replacement.replacesDelegationId,
|
|
274
263
|
targetSessionId: replacement.targetSessionId,
|
|
@@ -338,9 +327,44 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
338
327
|
};
|
|
339
328
|
}
|
|
340
329
|
}
|
|
341
|
-
async #
|
|
330
|
+
async #actFromTurn(input, context) {
|
|
331
|
+
let content;
|
|
342
332
|
try {
|
|
343
|
-
|
|
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
|
+
};
|
|
344
368
|
}
|
|
345
369
|
catch (error) {
|
|
346
370
|
if (error instanceof WorkHubActionEffectFailure) {
|
|
@@ -356,7 +380,11 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
356
380
|
return {
|
|
357
381
|
ok: false,
|
|
358
382
|
error: {
|
|
359
|
-
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',
|
|
360
388
|
message: error.message,
|
|
361
389
|
},
|
|
362
390
|
};
|
|
@@ -370,6 +398,23 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
370
398
|
};
|
|
371
399
|
}
|
|
372
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
|
+
}
|
|
373
418
|
#resolve() {
|
|
374
419
|
return this.#admission.run(WORKHUB_COORDINATION_SESSION_ID, async (lease) => {
|
|
375
420
|
// The workspace exists for provisioning and for reuse alike: a directory
|
|
@@ -436,19 +481,21 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
436
481
|
}
|
|
437
482
|
async #answer(input, context) {
|
|
438
483
|
if (!input.text.trim()) {
|
|
439
|
-
return turnFailure('
|
|
484
|
+
return turnFailure('operation_conflict', 'WorkHub answer text is empty');
|
|
440
485
|
}
|
|
441
486
|
const outcome = await this.#executions.startWorkHubCoordinationMessage({
|
|
442
487
|
sessionId: WORKHUB_COORDINATION_SESSION_ID,
|
|
443
488
|
turnId: input.turnId,
|
|
444
489
|
execution: {
|
|
445
490
|
kind: 'workhub_coordination',
|
|
446
|
-
inputDigest: digest({
|
|
491
|
+
inputDigest: digest({
|
|
492
|
+
text: input.text,
|
|
493
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
494
|
+
}),
|
|
447
495
|
},
|
|
448
496
|
archivedMessage: 'WorkHub Coordination Session is unavailable',
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
// 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.
|
|
452
499
|
prepareFreshContent: async () => {
|
|
453
500
|
let recorded;
|
|
454
501
|
try {
|
|
@@ -462,51 +509,18 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
462
509
|
}
|
|
463
510
|
return recorded.length > 0
|
|
464
511
|
? { kind: 'rejected', outcome: turnIdentityConflict() }
|
|
465
|
-
: {
|
|
512
|
+
: {
|
|
513
|
+
kind: 'ready',
|
|
514
|
+
content: normalizeMessageContent({
|
|
515
|
+
text: input.text,
|
|
516
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
517
|
+
}),
|
|
518
|
+
};
|
|
466
519
|
},
|
|
467
520
|
}, context);
|
|
468
521
|
return outcome.ok ? { ok: true, result: { turnId: input.turnId } } : outcome;
|
|
469
522
|
}
|
|
470
|
-
|
|
471
|
-
if (!input.userText.trim() || !input.assistantText.trim()) {
|
|
472
|
-
return Promise.resolve(turnFailure('workhub.coordination.record', 'operation_conflict', 'WorkHub Coordination summary text is empty'));
|
|
473
|
-
}
|
|
474
|
-
return this.#admission.run(WORKHUB_COORDINATION_SESSION_ID, async (lease) => {
|
|
475
|
-
let header;
|
|
476
|
-
try {
|
|
477
|
-
header = await this.#stores.readHeaderSnapshot(WORKHUB_COORDINATION_SESSION_ID);
|
|
478
|
-
}
|
|
479
|
-
catch {
|
|
480
|
-
return turnFailure('workhub.coordination.record', 'persistence_failed', 'WorkHub Coordination Session state is unavailable');
|
|
481
|
-
}
|
|
482
|
-
if (!validCoordinationHeader(header)) {
|
|
483
|
-
return turnFailure('workhub.coordination.record', 'operation_conflict', 'WorkHub Coordination Session identity is unavailable');
|
|
484
|
-
}
|
|
485
|
-
const messages = coordinationSummaryMessages(input);
|
|
486
|
-
try {
|
|
487
|
-
const existing = await this.#readSummaryMessages(input.turnId);
|
|
488
|
-
if (existing.length > 0) {
|
|
489
|
-
return coordinationSummaryMatches(existing, input)
|
|
490
|
-
? { ok: true, result: { turnId: input.turnId } }
|
|
491
|
-
: turnFailure('workhub.coordination.record', 'operation_conflict', 'WorkHub Coordination Turn identity belongs to different content');
|
|
492
|
-
}
|
|
493
|
-
// An answer owns its Turn identity in the root admission ledger. Both
|
|
494
|
-
// operations take the same Session admission, so this probe settles the
|
|
495
|
-
// race in one direction and the answer's own probe settles the other.
|
|
496
|
-
if (await this.#executions.hasRootTurnAdmission(WORKHUB_COORDINATION_SESSION_ID, input.turnId)) {
|
|
497
|
-
return turnFailure('workhub.coordination.record', 'operation_conflict', TURN_IDENTITY_CONFLICT_MESSAGE);
|
|
498
|
-
}
|
|
499
|
-
await this.#stores.appendMessages(WORKHUB_COORDINATION_SESSION_ID, messages);
|
|
500
|
-
await this.#continuity.refreshCanonical(WORKHUB_COORDINATION_SESSION_ID, lease);
|
|
501
|
-
return { ok: true, result: { turnId: input.turnId } };
|
|
502
|
-
}
|
|
503
|
-
catch {
|
|
504
|
-
this.#requestDrain();
|
|
505
|
-
return turnFailure('workhub.coordination.record', 'commit_outcome_unknown', 'WorkHub Coordination summary outcome is unknown');
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
/** Reads the durable summary triplet a `record` would own for this Turn. */
|
|
523
|
+
/** Reads a historical v1 summary to preserve its durable Turn identity. */
|
|
510
524
|
async #readSummaryMessages(turnId) {
|
|
511
525
|
const throughSequence = await this.#stores.readTranscriptHighWaterSnapshot(WORKHUB_COORDINATION_SESSION_ID);
|
|
512
526
|
if (throughSequence === null)
|
|
@@ -526,32 +540,52 @@ export class HostWorkHubCoordinationCoordinator {
|
|
|
526
540
|
*/
|
|
527
541
|
async #alignSession(record) {
|
|
528
542
|
if (record.header.toolProfile !== undefined &&
|
|
543
|
+
record.header.toolProfile !== 'workhub-coordination-v1' &&
|
|
529
544
|
record.header.toolProfile !== COORDINATION_TOOL_PROFILE) {
|
|
530
545
|
return identityConflict();
|
|
531
546
|
}
|
|
532
|
-
if (record.header.cwd === this.#coordinationCwd &&
|
|
547
|
+
if (record.header.cwd === this.#coordinationCwd &&
|
|
548
|
+
record.header.toolProfile === COORDINATION_TOOL_PROFILE &&
|
|
549
|
+
validCoordinationHeader(record.header)) {
|
|
533
550
|
return success();
|
|
534
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
|
+
}
|
|
535
559
|
let repaired;
|
|
536
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
|
+
});
|
|
537
580
|
repaired = await this.#stores.updateHeaderVersioned(WORKHUB_COORDINATION_SESSION_ID, {
|
|
538
581
|
...(record.header.cwd === this.#coordinationCwd ? {} : { cwd: this.#coordinationCwd }),
|
|
539
582
|
...(record.header.toolProfile === COORDINATION_TOOL_PROFILE
|
|
540
583
|
? {}
|
|
541
584
|
: { toolProfile: COORDINATION_TOOL_PROFILE }),
|
|
542
|
-
|
|
543
|
-
? {}
|
|
544
|
-
: { permissionMode: COORDINATION_PERMISSION_MODE }),
|
|
545
|
-
...((record.header.collaborationMode ?? 'agent') === COORDINATION_COLLABORATION_MODE
|
|
546
|
-
? {}
|
|
547
|
-
: { collaborationMode: COORDINATION_COLLABORATION_MODE }),
|
|
548
|
-
...((record.header.orchestrationMode ?? 'default') === COORDINATION_ORCHESTRATION_MODE
|
|
549
|
-
? {}
|
|
550
|
-
: { orchestrationMode: COORDINATION_ORCHESTRATION_MODE }),
|
|
551
|
-
}, record.revision);
|
|
585
|
+
}, configured.revision);
|
|
552
586
|
}
|
|
553
587
|
catch {
|
|
554
|
-
return failure('persistence_failed', 'WorkHub Coordination Session
|
|
588
|
+
return failure('persistence_failed', 'WorkHub Coordination Session configuration could not be aligned');
|
|
555
589
|
}
|
|
556
590
|
return validCoordinationHeader(repaired.header) && repaired.header.cwd === this.#coordinationCwd
|
|
557
591
|
? success()
|
|
@@ -577,8 +611,9 @@ function validCoordinationIdentityHeader(header) {
|
|
|
577
611
|
}
|
|
578
612
|
function validCoordinationHeader(header) {
|
|
579
613
|
return (validCoordinationIdentityHeader(header) &&
|
|
580
|
-
header.toolProfile === COORDINATION_TOOL_PROFILE &&
|
|
581
|
-
|
|
614
|
+
((header.toolProfile === COORDINATION_TOOL_PROFILE &&
|
|
615
|
+
header.permissionMode === COORDINATION_PERMISSION_MODE) ||
|
|
616
|
+
(header.toolProfile === 'workhub-coordination-v1' && header.permissionMode === 'explore')) &&
|
|
582
617
|
(header.collaborationMode ?? 'agent') === COORDINATION_COLLABORATION_MODE &&
|
|
583
618
|
(header.orchestrationMode ?? 'default') === COORDINATION_ORCHESTRATION_MODE);
|
|
584
619
|
}
|
|
@@ -594,48 +629,6 @@ function coordinationSummaryMessageId(turnId, kind) {
|
|
|
594
629
|
.digest('hex')
|
|
595
630
|
.slice(0, 48)}`;
|
|
596
631
|
}
|
|
597
|
-
function coordinationSummaryMessages(input) {
|
|
598
|
-
const ts = Date.now();
|
|
599
|
-
const messageId = (kind) => coordinationSummaryMessageId(input.turnId, kind);
|
|
600
|
-
return [
|
|
601
|
-
{
|
|
602
|
-
type: 'user',
|
|
603
|
-
id: messageId('user'),
|
|
604
|
-
turnId: input.turnId,
|
|
605
|
-
ts,
|
|
606
|
-
text: input.userText,
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
type: 'assistant',
|
|
610
|
-
id: messageId('assistant'),
|
|
611
|
-
turnId: input.turnId,
|
|
612
|
-
ts: ts + 1,
|
|
613
|
-
text: input.assistantText,
|
|
614
|
-
modelId: SYNTHETIC_COORDINATION_MODEL_ID,
|
|
615
|
-
},
|
|
616
|
-
{
|
|
617
|
-
type: 'turn_state',
|
|
618
|
-
id: messageId('state'),
|
|
619
|
-
turnId: input.turnId,
|
|
620
|
-
ts: ts + 2,
|
|
621
|
-
status: 'completed',
|
|
622
|
-
},
|
|
623
|
-
];
|
|
624
|
-
}
|
|
625
|
-
function coordinationSummaryMatches(existing, input) {
|
|
626
|
-
if (existing.length !== 3)
|
|
627
|
-
return false;
|
|
628
|
-
const user = existing.find((message) => message.type === 'user');
|
|
629
|
-
const assistant = existing.find((message) => message.type === 'assistant');
|
|
630
|
-
const state = existing.find((message) => message.type === 'turn_state');
|
|
631
|
-
return (user?.turnId === input.turnId &&
|
|
632
|
-
user.text === input.userText &&
|
|
633
|
-
assistant?.turnId === input.turnId &&
|
|
634
|
-
assistant.text === input.assistantText &&
|
|
635
|
-
assistant.modelId === SYNTHETIC_COORDINATION_MODEL_ID &&
|
|
636
|
-
state?.turnId === input.turnId &&
|
|
637
|
-
state.status === 'completed');
|
|
638
|
-
}
|
|
639
632
|
function success() {
|
|
640
633
|
return {
|
|
641
634
|
ok: true,
|
|
@@ -658,6 +651,6 @@ function turnIdentityConflict() {
|
|
|
658
651
|
function operationUnavailable(message) {
|
|
659
652
|
return { ok: false, error: { code: 'operation_unavailable', message } };
|
|
660
653
|
}
|
|
661
|
-
function turnFailure(
|
|
654
|
+
function turnFailure(code, message) {
|
|
662
655
|
return { ok: false, error: { code, message } };
|
|
663
656
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
|
|
20
|
+
import { WorkHubActionEffectFailure } from './workhub-coordination-action-gate.js';
|
|
21
|
+
/** Preserve canonical ownership when a Coordination message is delegated. */
|
|
22
|
+
export async function copyWorkHubAttachmentsToTarget(store, artifacts, targetSessionId, attachments) {
|
|
23
|
+
const invalid = await artifacts.validateTurnAttachments(WORKHUB_COORDINATION_SESSION_ID, attachments);
|
|
24
|
+
if (invalid)
|
|
25
|
+
throw new WorkHubActionEffectFailure('operation_conflict', invalid);
|
|
26
|
+
const ids = attachments.map((attachment) => {
|
|
27
|
+
if (attachment.ref.kind !== 'session_file')
|
|
28
|
+
throw new Error('Invalid WorkHub attachment');
|
|
29
|
+
return attachment.ref.relativePath;
|
|
30
|
+
});
|
|
31
|
+
const copied = await store.copyConversationArtifacts({
|
|
32
|
+
sourceSessionId: WORKHUB_COORDINATION_SESSION_ID,
|
|
33
|
+
targetSessionId,
|
|
34
|
+
turnIds: [],
|
|
35
|
+
includeArtifactIds: ids,
|
|
36
|
+
});
|
|
37
|
+
return attachments.map((attachment, index) => {
|
|
38
|
+
const relativePath = copied.artifactIds.get(ids[index]);
|
|
39
|
+
if (!relativePath)
|
|
40
|
+
throw new WorkHubActionEffectFailure('operation_conflict', 'WorkHub attachment was not copied');
|
|
41
|
+
return {
|
|
42
|
+
...attachment,
|
|
43
|
+
ref: { kind: 'session_file', sessionId: targetSessionId, relativePath },
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|