@playcraft/cli 0.1.57-beta → 0.1.59-beta
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/commands/workspace.js +43 -0
- package/dist/utils/agent-generation-provenance.js +1 -1
- package/dist/workspace/messages.js +10 -0
- package/dist/workspace-runtime/batch/deliver.js +140 -80
- package/dist/workspace-runtime/batch/freeze.js +18 -6
- package/dist/workspace-runtime/batch/generate.js +68 -64
- package/dist/workspace-runtime/batch/policy.js +71 -22
- package/dist/workspace-runtime/batch/prepare.js +108 -93
- package/dist/workspace-runtime/batch/records.js +4 -0
- package/dist/workspace-runtime/batch/routes.js +10 -2
- package/dist/workspace-runtime/batch/source-candidate.js +2 -0
- package/dist/workspace-runtime/batch/source-capture.js +1 -0
- package/dist/workspace-runtime/batch/source-failure.js +2 -0
- package/dist/workspace-runtime/batch/source-publish.js +3 -0
- package/dist/workspace-runtime/batch/writer-proof.js +37 -0
- package/dist/workspace-runtime/codex/app-server.js +278 -20
- package/dist/workspace-runtime/codex/config-toml.js +4 -0
- package/dist/workspace-runtime/codex/delivery-tools.js +1 -1
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +17 -8
- package/dist/workspace-runtime/codex/loopback.js +2 -0
- package/dist/workspace-runtime/codex/model-broker.js +7 -4
- package/dist/workspace-runtime/persistence/journal.js +34 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
- package/dist/workspace-runtime/previews/output.js +2 -0
- package/dist/workspace-runtime/previews/publish.js +20 -5
- package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
- package/dist/workspace-runtime/processes/managed-writes.js +47 -5
- package/dist/workspace-runtime/serve.js +13 -3
- package/dist/workspace-runtime/server/accept-operation.js +22 -2
- package/dist/workspace-runtime/server/directory-access.js +414 -0
- package/dist/workspace-runtime/server/dispatch.js +74 -29
- package/dist/workspace-runtime/server/endpoints.js +1 -0
- package/dist/workspace-runtime/server/http.js +80 -10
- package/dist/workspace-runtime/server/operation-record.js +2 -0
- package/dist/workspace-runtime/server/terminal-host.js +124 -8
- package/dist/workspace-runtime/server/workspace-storage.js +193 -0
- package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
- package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
- package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
- package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
- package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
- package/dist/workspace-runtime/workspaces/attachments.js +9 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
- package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
- package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
- package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +90 -70
- package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
- package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
- package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
- package/dist/workspace-runtime/workspaces/files.js +4 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
- package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
- package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
- package/dist/workspace-runtime/workspaces/parameters.js +95 -3
- package/dist/workspace-runtime/workspaces/platform-export.js +5 -0
- package/dist/workspace-runtime/workspaces/prepare.js +4 -0
- package/dist/workspace-runtime/workspaces/registry.js +55 -3
- package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
- package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
- package/dist/workspace-runtime/workspaces/skills.js +14 -15
- package/dist/workspace-runtime/workspaces/storage-snapshot.js +235 -0
- package/dist/workspace-runtime/workspaces/transfer.js +4 -1
- package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
- package/package.json +5 -4
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
|
@@ -2,6 +2,7 @@ import { withObservedFile } from '../workspaces/revisions.js';
|
|
|
2
2
|
import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
|
|
3
3
|
import { abandonTurnChanges, beginTurnChanges, finishTurnChanges } from '../workspaces/turn-changes.js';
|
|
4
4
|
import { shareWorkspaceGit } from '../workspaces/shared-git.js';
|
|
5
|
+
import { holdDirectoryAccess, withOperationDirectoryAccess } from './directory-access.js';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, deliveryBuildPayloadSchema, deliveryCancelPayloadSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
@@ -11,6 +12,7 @@ import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
|
11
12
|
import { resolveWorkspaceAttachments } from '../workspaces/attachments.js';
|
|
12
13
|
import { importWorkspaceRemixReference, RemixReferenceError } from '../workspaces/remix-references.js';
|
|
13
14
|
import { ComposerContextError, resolveComposerContext } from '../workspaces/composer-context.js';
|
|
15
|
+
import { ensureParameterInitializationSkill } from '../workspaces/parameter-initialization-skill.js';
|
|
14
16
|
import { readWorkspaceSkillSnapshot, readWorkspaceSkills } from '../workspaces/skills.js';
|
|
15
17
|
import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess } from '../workspaces/skill-artifact-loader.js';
|
|
16
18
|
import { enableLocalProjectSkill, syncLocalProjectSkills } from '../../project-skills/local-store.js';
|
|
@@ -28,9 +30,10 @@ import { ThemeAssetIndexError } from '../workspaces/theme-asset-index.js';
|
|
|
28
30
|
import { patchWorkspaceParameters, readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
29
31
|
import { prepareParameterAgentTask, finishParameterAgentTask, verifyParameterAgentTask, } from '../workspaces/parameter-agent.js';
|
|
30
32
|
import { previewFailureCode, previewWorkspaceRoot, runPreviewPublish, trackPreviewPublish, } from '../previews/publish.js';
|
|
31
|
-
import {
|
|
33
|
+
import { startManagedBackgroundWriter, stopBackgroundWriter, } from '../processes/managed-writes.js';
|
|
32
34
|
import { wakeSourceEventUploader } from './source-uploader.js';
|
|
33
35
|
import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
36
|
+
import { trustedMemberPolicy } from '../batch/policy.js';
|
|
34
37
|
import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile } from '../workspaces/binary-write.js';
|
|
35
38
|
import { importWorkspaceLibraryAssets } from '../workspaces/asset-library-import.js';
|
|
36
39
|
import { createWorkspaceDirectory, WorkspaceDirectoryCreateError } from '../workspaces/directory-create.js';
|
|
@@ -90,6 +93,8 @@ export async function recoverInterruptedParameterVerifications(journal) {
|
|
|
90
93
|
for (const { scopeKey, operation } of journal.listOperations()) {
|
|
91
94
|
if (operation.kind !== 'parameter.verify' || !['queued', 'dispatched', 'accepted', 'uncertain'].includes(operation.status))
|
|
92
95
|
continue;
|
|
96
|
+
if (operation.status === 'queued' && journal.deferredDirectoryOperations().some(item => item.operationId === operation.operationId))
|
|
97
|
+
continue;
|
|
93
98
|
await journal.putOperation(scopeKey, transitionOperation(operation, 'interrupted', { error: 'parameter_verification_interrupted' }));
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -105,12 +110,21 @@ export async function executeOperation(state, scopeKey, operation, payload, reco
|
|
|
105
110
|
if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed()) {
|
|
106
111
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: state.journal.destroyed() ? 'runtime_destroyed' : 'stale_lease' }));
|
|
107
112
|
}
|
|
108
|
-
|
|
113
|
+
if (operation.kind === 'session.turn' && !recovery && operation.sessionId &&
|
|
114
|
+
state.journal.getSession(operation.sessionId)?.recoverability === 'history_only')
|
|
115
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'native_not_recoverable' }));
|
|
116
|
+
return withOperationDirectoryAccess(state, operation, () => {
|
|
117
|
+
if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed())
|
|
118
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: state.journal.destroyed() ? 'runtime_destroyed' : 'stale_lease' }));
|
|
119
|
+
return executeScopedOperation(state, scopeKey, operation, payload, recovery);
|
|
120
|
+
}, { operationId: operation.operationId, scopeKey, payload, ...(recovery ? { recovery } : {}) });
|
|
109
121
|
};
|
|
110
122
|
if (['runtime.destroy', 'session.interrupt', 'session.approval.respond', 'session.steer', 'preview.publish', 'delivery.build', 'delivery.cancel'].includes(operation.kind)) {
|
|
111
123
|
return run();
|
|
112
124
|
}
|
|
113
|
-
return state.journal.exclusive(
|
|
125
|
+
return state.journal.exclusive(operation.kind.startsWith('session.') && operation.sessionId
|
|
126
|
+
? `session-execution:${operation.sessionId}`
|
|
127
|
+
: `workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`, run);
|
|
114
128
|
}
|
|
115
129
|
async function executeScopedOperation(state, scopeKey, operation, payload, recovery) {
|
|
116
130
|
let current = await persist(state, scopeKey, transitionOperation(operation, 'dispatched'));
|
|
@@ -204,7 +218,7 @@ async function executeScopedOperation(state, scopeKey, operation, payload, recov
|
|
|
204
218
|
if (operation.kind === 'session.steer')
|
|
205
219
|
return executeSteer(state, scopeKey, current, payload);
|
|
206
220
|
if (operation.kind === 'session.interrupt') {
|
|
207
|
-
return executeInterrupt(state, scopeKey, current);
|
|
221
|
+
return executeInterrupt(state, scopeKey, current, payload);
|
|
208
222
|
}
|
|
209
223
|
if (operation.kind === 'session.approval.respond') {
|
|
210
224
|
return executeApproval(state, scopeKey, current, payload);
|
|
@@ -381,12 +395,12 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
381
395
|
}
|
|
382
396
|
found = rebound;
|
|
383
397
|
}
|
|
384
|
-
const binding = found;
|
|
385
398
|
// Also covers a new thread whose first turn failed before starting: the next turn still carries it.
|
|
386
|
-
const context = recovery &&
|
|
387
|
-
if (!
|
|
399
|
+
const context = recovery && found?.recoveryContextPending ? recovery.context : '';
|
|
400
|
+
if (!found) {
|
|
388
401
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unknown_session' }));
|
|
389
402
|
}
|
|
403
|
+
let binding = found;
|
|
390
404
|
if ((binding.directoryName ?? 'basic') !== (operation.directoryName ?? 'basic')) {
|
|
391
405
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'context_directory_mismatch' }));
|
|
392
406
|
}
|
|
@@ -398,17 +412,30 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
398
412
|
}
|
|
399
413
|
const root = await sessionWorkspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
400
414
|
let composer;
|
|
415
|
+
let initializationSkill;
|
|
401
416
|
try {
|
|
417
|
+
let turnContext = selection.data.context;
|
|
418
|
+
if (parameterTask.data?.action === 'initialize') {
|
|
419
|
+
const selectedSkill = await ensureParameterInitializationSkill(root);
|
|
420
|
+
initializationSkill = { ...selectedSkill,
|
|
421
|
+
path: join(root, '.playcraft/skills', selectedSkill.lockHash, selectedSkill.entryKey, 'SKILL.md') };
|
|
422
|
+
turnContext = { ...turnContext, files: turnContext?.files ?? [], skills: [
|
|
423
|
+
...(turnContext?.skills ?? []).filter(skill => skill.entryKey !== selectedSkill.entryKey), selectedSkill,
|
|
424
|
+
] };
|
|
425
|
+
}
|
|
402
426
|
composer = await resolveComposerContext({
|
|
403
427
|
root,
|
|
404
|
-
directoryName: binding.directoryName ?? 'basic', context:
|
|
428
|
+
directoryName: binding.directoryName ?? 'basic', context: turnContext,
|
|
405
429
|
skillCache: join(state.config.stateDir, 'skill-artifacts'),
|
|
406
430
|
loadSkillArtifact: entry => loadWorkspaceSkillArtifact(state, operation.workspaceId ?? 'workspace', entry),
|
|
407
431
|
});
|
|
408
432
|
}
|
|
409
433
|
catch (error) {
|
|
410
434
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', {
|
|
411
|
-
error: error instanceof ComposerContextError ? error.message : '
|
|
435
|
+
error: error instanceof ComposerContextError ? error.message : parameterTask.data?.action === 'initialize'
|
|
436
|
+
? error instanceof Error && /^(?:workspace_skills_|skills_revision_conflict$)/.test(error.message)
|
|
437
|
+
? error.message : parameterAgentError(error)
|
|
438
|
+
: 'invalid_composer_context',
|
|
412
439
|
}));
|
|
413
440
|
}
|
|
414
441
|
let attachments;
|
|
@@ -429,7 +456,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
429
456
|
...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}\n\n`
|
|
430
457
|
: '';
|
|
431
458
|
const engineeringBatchContext = state.config.workspaceMode === 'multi' && operation.workspaceId
|
|
432
|
-
&& operation.directoryName !== 'root' && !productionSessionAttemptId(sessionId)
|
|
459
|
+
&& operation.directoryName !== 'root' && !productionSessionAttemptId(sessionId) && !trustedMemberPolicy(state, sessionId)
|
|
433
460
|
? `PlayCraft batch request context (trusted runtime identity): ${JSON.stringify({ workspaceId: operation.workspaceId,
|
|
434
461
|
clientRequestId: operation.clientRequestId, sourceDirectoryName: binding.directoryName ?? 'basic',
|
|
435
462
|
...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}`
|
|
@@ -446,7 +473,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
446
473
|
let parameterInstructions = '';
|
|
447
474
|
if (parameterTask.data) {
|
|
448
475
|
try {
|
|
449
|
-
parameterInstructions = (await prepareParameterAgentTask(state, operation, parameterTask.data, root)).instructions;
|
|
476
|
+
parameterInstructions = (await prepareParameterAgentTask(state, operation, parameterTask.data, root, { initializationSkill })).instructions;
|
|
450
477
|
}
|
|
451
478
|
catch (error) {
|
|
452
479
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: parameterAgentError(error) }));
|
|
@@ -458,7 +485,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
458
485
|
});
|
|
459
486
|
let result;
|
|
460
487
|
try {
|
|
461
|
-
|
|
488
|
+
const startTurn = () => native(state).startTurn({
|
|
462
489
|
sessionId,
|
|
463
490
|
threadId: binding.threadId,
|
|
464
491
|
text: composer.append(batchContext + context + String(body.text ?? '') + attachmentText, [engineeringBatchContext, parameterInstructions].filter(Boolean).join('\n\n')),
|
|
@@ -490,6 +517,18 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
490
517
|
await state.journal.putSession({ ...binding, modelConfigId, reasoningEffort: selectedEffort, permissionMode: selectedMode, turnStatus: 'starting' });
|
|
491
518
|
},
|
|
492
519
|
});
|
|
520
|
+
result = await startTurn();
|
|
521
|
+
// The native runtime cannot persist an empty thread before its first turn. Adding the
|
|
522
|
+
// selected skill can therefore require replacing that empty thread, before any model work.
|
|
523
|
+
if (initializationSkill && result.outcome === 'failed' && result.error === 'workspace_skills_changed'
|
|
524
|
+
&& !(state.journal.getOperation(scopeKey) ?? operation).nativeRequestId) {
|
|
525
|
+
const started = await native(state).startThread({ sessionId, cwd: root,
|
|
526
|
+
modelConfigId: selected.data ?? binding.modelConfigId, reasoningEffort: requestedEffort, permissionMode: requestedMode });
|
|
527
|
+
binding = { ...binding, threadId: started.threadId, modelConfigId: started.modelConfigId ?? binding.modelConfigId,
|
|
528
|
+
reasoningEffort: started.reasoningEffort ?? requestedEffort, permissionMode: started.permissionMode ?? requestedMode };
|
|
529
|
+
await state.journal.putSession(binding);
|
|
530
|
+
result = await startTurn();
|
|
531
|
+
}
|
|
493
532
|
}
|
|
494
533
|
catch (error) {
|
|
495
534
|
if (!parameterTask.data)
|
|
@@ -811,13 +850,18 @@ async function executeSteer(state, scopeKey, operation, payload) {
|
|
|
811
850
|
} }]);
|
|
812
851
|
return persist(state, scopeKey, transitionOperation(accepted, 'completed'));
|
|
813
852
|
}
|
|
814
|
-
async function executeInterrupt(state, scopeKey, operation) {
|
|
853
|
+
async function executeInterrupt(state, scopeKey, operation, payload) {
|
|
815
854
|
const sessionId = required(operation.sessionId, 'sessionId');
|
|
816
855
|
const binding = state.journal.getSession(sessionId);
|
|
817
856
|
if (!binding) {
|
|
818
857
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unknown_session' }));
|
|
819
858
|
}
|
|
820
|
-
const
|
|
859
|
+
const expectedTurnId = asRecord(payload).expectedTurnId;
|
|
860
|
+
if (expectedTurnId !== undefined && (typeof expectedTurnId !== 'string' || !expectedTurnId
|
|
861
|
+
|| binding.activeTurnId !== expectedTurnId))
|
|
862
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_changed' }));
|
|
863
|
+
const result = await native(state).interrupt({ sessionId, threadId: binding.threadId,
|
|
864
|
+
...(typeof expectedTurnId === 'string' ? { expectedTurnId } : {}) });
|
|
821
865
|
if (result.outcome === 'failed') {
|
|
822
866
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: result.error ?? 'turn_not_running' }));
|
|
823
867
|
}
|
|
@@ -1113,7 +1157,7 @@ async function executeFileWrite(state, scopeKey, operation, payload) {
|
|
|
1113
1157
|
: 'invalid_workspace_path';
|
|
1114
1158
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error }));
|
|
1115
1159
|
}
|
|
1116
|
-
const workspaceId = operation.workspaceId ?? 'workspace';
|
|
1160
|
+
const workspaceId = revisionScope(state, operation.workspaceId, operation.directoryName) ?? operation.workspaceId ?? 'workspace';
|
|
1117
1161
|
const writer = parsed.data.writer ?? 'manual';
|
|
1118
1162
|
if (parsed.data.release) {
|
|
1119
1163
|
const lease = state.journal.getManagedWrite(parsed.data.writeId);
|
|
@@ -1147,17 +1191,14 @@ async function executeFileWrite(state, scopeKey, operation, payload) {
|
|
|
1147
1191
|
contentHash: written.contentHash,
|
|
1148
1192
|
}, revisionScope(state, operation.workspaceId, operation.directoryName));
|
|
1149
1193
|
if (parsed.data.hold && parsed.data.writeId) {
|
|
1150
|
-
|
|
1194
|
+
const record = { writeId: parsed.data.writeId, workspaceId, source: writer, startedAt: new Date().toISOString() };
|
|
1151
1195
|
if (writer === 'background') {
|
|
1152
|
-
|
|
1196
|
+
await startManagedBackgroundWriter(state, record, await resolveWorkspaceFile(root, parsed.data.path));
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1199
|
+
await holdDirectoryAccess(state, operation, `managed:${parsed.data.writeId}`, 'shared');
|
|
1200
|
+
await state.journal.putManagedWrite(record);
|
|
1153
1201
|
}
|
|
1154
|
-
await state.journal.putManagedWrite({
|
|
1155
|
-
writeId: parsed.data.writeId,
|
|
1156
|
-
workspaceId,
|
|
1157
|
-
source: writer,
|
|
1158
|
-
startedAt: new Date().toISOString(),
|
|
1159
|
-
pid,
|
|
1160
|
-
});
|
|
1161
1202
|
}
|
|
1162
1203
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { expectedRevision: revision }));
|
|
1163
1204
|
if (artifactFormat(parsed.data.path) && operation.workspaceId) {
|
|
@@ -1238,11 +1279,12 @@ async function executeWorkspaceSave(state, scopeKey, operation, payload) {
|
|
|
1238
1279
|
}
|
|
1239
1280
|
const binding = operation.workspaceId ? state.journal.getWorkspace(operation.workspaceId) : undefined;
|
|
1240
1281
|
try {
|
|
1241
|
-
|
|
1282
|
+
// Saves share the directory, and a deferred one runs outside the workspace queue; commit one at a time.
|
|
1283
|
+
const saved = await state.journal.exclusive(`workspace-save:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`, async () => saveWorkspaceGit(await gitWorkspaceRoot(state, operation), {
|
|
1242
1284
|
message: parsed.data.trigger === 'auto' ? 'Autosave workspace' : 'Save workspace',
|
|
1243
1285
|
push: savePushesBranch(binding?.targetKind, operation.directoryName),
|
|
1244
1286
|
boundBranch: boundBranch(state, operation),
|
|
1245
|
-
});
|
|
1287
|
+
}));
|
|
1246
1288
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
1247
1289
|
await recordNativeEvents(state, accepted, [
|
|
1248
1290
|
{
|
|
@@ -1349,7 +1391,9 @@ async function executeParameterPatch(state, scopeKey, operation, payload) {
|
|
|
1349
1391
|
if (document.status === 'missing' || document.status === 'schema_only' || !document.dataPath) {
|
|
1350
1392
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_data_missing' }));
|
|
1351
1393
|
}
|
|
1352
|
-
const dataPath = document.dataPath;
|
|
1394
|
+
const dataPath = 'documentId' in parsed.data ? document.runtimeI18n?.themePath : document.dataPath;
|
|
1395
|
+
if (!dataPath)
|
|
1396
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_i18n_global_invalid' }));
|
|
1353
1397
|
return withObservedFile(state, operation.workspaceId, dataPath, async ({ revision: currentRevision, file }) => {
|
|
1354
1398
|
if (parsed.data.expectedRevision !== currentRevision) {
|
|
1355
1399
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'revision_conflict' }));
|
|
@@ -1386,6 +1430,7 @@ async function executeParameterPatch(state, scopeKey, operation, payload) {
|
|
|
1386
1430
|
revision,
|
|
1387
1431
|
contentHash: written.contentHash,
|
|
1388
1432
|
size: written.size,
|
|
1433
|
+
...('documentId' in parsed.data ? { documentId: parsed.data.documentId } : {}),
|
|
1389
1434
|
...('changes' in parsed.data
|
|
1390
1435
|
? { fieldPaths: parsed.data.changes.map((change) => change.fieldPath) }
|
|
1391
1436
|
: { fieldPath: parsed.data.fieldPath }),
|
|
@@ -1416,7 +1461,7 @@ async function executePreviewPublish(state, scopeKey, operation) {
|
|
|
1416
1461
|
trackPreviewPublish(state, runPreviewPublish(state, accepted, root).then(async () => {
|
|
1417
1462
|
await persist(state, scopeKey, transitionOperation(accepted, 'completed'));
|
|
1418
1463
|
}, async (error) => {
|
|
1419
|
-
await persist(state, scopeKey, transitionOperation(accepted, 'failed', { error: previewFailureCode(error) }));
|
|
1464
|
+
await persist(state, scopeKey, transitionOperation(accepted, previewFailureCode(error) === 'preview_process_unknown' ? 'uncertain' : 'failed', { error: previewFailureCode(error) }));
|
|
1420
1465
|
}).catch(() => undefined));
|
|
1421
1466
|
return accepted;
|
|
1422
1467
|
}
|
|
@@ -1438,7 +1483,7 @@ async function executeDeliveryBuild(state, scopeKey, operation, payload) {
|
|
|
1438
1483
|
}).then(async (result) => {
|
|
1439
1484
|
const error = result.record.error
|
|
1440
1485
|
?? (result.record.status === 'succeeded' ? undefined : 'delivery_build_failed');
|
|
1441
|
-
await persist(state, scopeKey, transitionOperation(accepted, result.record.status === 'succeeded' ? 'completed' : 'failed', error ? { error } : {}));
|
|
1486
|
+
await persist(state, scopeKey, transitionOperation(accepted, result.record.status === 'succeeded' ? 'completed' : error === 'delivery_process_unknown' ? 'uncertain' : 'failed', error ? { error } : {}));
|
|
1442
1487
|
}, async (error) => {
|
|
1443
1488
|
await persist(state, scopeKey, transitionOperation(accepted, 'failed', {
|
|
1444
1489
|
error: error instanceof DeliveryBuildError ? error.code : 'delivery_build_failed',
|
|
@@ -12,6 +12,7 @@ export async function applyRuntimeEndpointPatch(state, patch) {
|
|
|
12
12
|
throw new Error('endpoint_config_path_missing');
|
|
13
13
|
}
|
|
14
14
|
const temporary = `${state.configPath}.${process.pid}.tmp`;
|
|
15
|
+
// private
|
|
15
16
|
await writeFile(temporary, `${JSON.stringify(next)}\n`, { mode: 0o600 });
|
|
16
17
|
await chmod(temporary, 0o600);
|
|
17
18
|
await rename(temporary, state.configPath);
|
|
@@ -3,7 +3,7 @@ import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.j
|
|
|
3
3
|
import { registerWorkspace, revisionScope, sessionWorkspaceRoot, workspaceRoot, WorkspaceContextError } from '../workspaces/registry.js';
|
|
4
4
|
import { constants } from 'node:fs';
|
|
5
5
|
import { access as accessDirectory, stat } from 'node:fs/promises';
|
|
6
|
-
import { workspaceRegistrationRequestSchema, workspaceFileSearchQuerySchema, HTTP_STATUS_BY_ERROR, assertLeaseFence, buildHandshakeDocument, capability, findUntrustedRuntimeFields, gitFileRefSchema, sourceAckSchema, operationAcceptRequestSchema, operationLogSchema, operationKindSchema, operationScopeKey, productionSessionAttemptId, takesWorkspaceDirectory, nativeHistoryReadSchema, runtimeError, runtimeConnectivityDocumentSchema, unavailableRuntimeCapabilities, workspaceUiResultSchema, workspaceRemixReferenceImportRequestSchema, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES, WORKSPACE_ASSET_LIBRARY_IMPORT_BODY_MAX_BYTES, workspaceSessionRestoreRequestSchema, workspaceSessionRestoreResponseSchema, deliveryValidatePayloadSchema, deliveryBuildListQuerySchema, deliveryBuildListSchema, deliveryBuildDetailSchema, deliveryBuildLogSchema, workspaceDirectoryNameSchema, attachmentIdsSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
|
+
import { workspaceRegistrationRequestSchema, workspaceFileSearchQuerySchema, HTTP_STATUS_BY_ERROR, assertLeaseFence, buildHandshakeDocument, capability, findUntrustedRuntimeFields, gitFileRefSchema, sourceAckSchema, operationAcceptRequestSchema, operationLogSchema, operationKindSchema, operationScopeKey, productionSessionAttemptId, takesWorkspaceDirectory, nativeHistoryReadSchema, runtimeError, runtimeConnectivityDocumentSchema, unavailableRuntimeCapabilities, workspaceUiResultSchema, workspaceRemixReferenceImportRequestSchema, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES, WORKSPACE_ASSET_LIBRARY_IMPORT_BODY_MAX_BYTES, workspaceSessionRestoreRequestSchema, workspaceSessionRestoreResponseSchema, deliveryValidatePayloadSchema, deliveryBuildListQuerySchema, deliveryBuildListSchema, deliveryBuildDetailSchema, deliveryBuildLogSchema, workspaceDirectoryNameSchema, workspaceDirectoryRegistrationSchema, attachmentIdsSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
7
|
import { controlTokenMatches, extractControlToken } from './auth.js';
|
|
8
8
|
import { applyRuntimeEndpointPatch } from './endpoints.js';
|
|
9
9
|
import { wakeSourceEventUploader } from './source-uploader.js';
|
|
@@ -12,6 +12,7 @@ import { GitWorkspaceError, readWorkspaceGitDiff, readWorkspaceGitFile, readWork
|
|
|
12
12
|
import { readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
13
13
|
import { listParameterAgentTasks } from '../workspaces/parameter-agent.js';
|
|
14
14
|
import { readDependencies } from '../workspaces/dependencies.js';
|
|
15
|
+
import { permissionDriftFromError } from '../workspaces/workspace-shares.js';
|
|
15
16
|
import { desktopHost, desktopRoute } from './desktop-host.js';
|
|
16
17
|
import { ensureCliCredentials } from '../workspaces/cli-credentials.js';
|
|
17
18
|
import { readAttachmentUpload, uploadWorkspaceAttachment, uploadWorkspaceVideoAttachment } from '../workspaces/attachments.js';
|
|
@@ -40,11 +41,37 @@ import { workspaceArtifactContentQuerySchema } from '@playcraft/common/cloud-wor
|
|
|
40
41
|
import { searchWorkspaceFiles } from '../workspaces/files-search.js';
|
|
41
42
|
import { serveWorkspaceArchive } from '../workspaces/archive.js';
|
|
42
43
|
import { handleWorkspaceTransfer } from './workspace-transfer.js';
|
|
44
|
+
import { handleWorkspaceStorage } from './workspace-storage.js';
|
|
43
45
|
import { serveSessionImage } from '../workspaces/session-image.js';
|
|
44
46
|
import { restoreNativeSession, SessionRestoreError } from './session-restore.js';
|
|
45
47
|
import { handleAssetTransferRequest } from './asset-transfers.js';
|
|
46
48
|
import { readWorktreeSource, registerChatWorktree } from '../workspaces/worktrees.js';
|
|
49
|
+
import { withDirectoryWrite } from './directory-access.js';
|
|
47
50
|
export async function handleRuntimeRequest(state, request, response) {
|
|
51
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
52
|
+
const workspaceId = url.searchParams.get('workspaceId');
|
|
53
|
+
// Body-scoped operations, members and terminal control take their own lifetime-aware leases.
|
|
54
|
+
const ownLease = url.pathname.startsWith('/runtime/operations') || url.pathname.startsWith('/runtime/terminals/') ||
|
|
55
|
+
url.pathname.startsWith('/runtime/batch-') || /^\/runtime\/sessions\/[^/]+\/ui-results$/.test(url.pathname) ||
|
|
56
|
+
/^\/runtime\/sessions\/[^/]+\/prediction$/.test(url.pathname);
|
|
57
|
+
if (authorizeRuntime(state, request) === 'control' && workspaceId &&
|
|
58
|
+
(!['GET', 'HEAD', 'OPTIONS'].includes(request.method ?? 'GET') || url.pathname === '/runtime/git-bundle') && !ownLease) {
|
|
59
|
+
try {
|
|
60
|
+
return await withDirectoryWrite(state, { workspaceId, directoryName: url.searchParams.get('directoryName') ?? 'basic', wait: false,
|
|
61
|
+
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
62
|
+
...(url.pathname === '/runtime/git-bundle' ? { mode: 'exclusive' } : {}) }, () => dispatchRuntimeRequest(state, request, response));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!response.headersSent)
|
|
66
|
+
writeJson(response, error instanceof Error && error.message === 'directory_busy' ? 409 : 500, { error: error instanceof Error && error.message === 'directory_busy' ? 'directory_busy' : 'runtime_request_failed' });
|
|
67
|
+
else
|
|
68
|
+
response.destroy(error instanceof Error ? error : undefined);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return dispatchRuntimeRequest(state, request, response);
|
|
73
|
+
}
|
|
74
|
+
async function dispatchRuntimeRequest(state, request, response) {
|
|
48
75
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
49
76
|
const directoryName = url.searchParams.get('directoryName') ?? 'basic';
|
|
50
77
|
const access = authorizeRuntime(state, request);
|
|
@@ -57,6 +84,8 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
57
84
|
return;
|
|
58
85
|
}
|
|
59
86
|
try {
|
|
87
|
+
if (await handleWorkspaceStorage(state, request, response, url))
|
|
88
|
+
return;
|
|
60
89
|
const transfer = await handleAssetTransferRequest(state, request, url);
|
|
61
90
|
if (transfer) {
|
|
62
91
|
writeJson(response, transfer.status, transfer.body);
|
|
@@ -103,12 +132,17 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
103
132
|
instanceToken: state.instanceToken,
|
|
104
133
|
permissionsVersion: 1,
|
|
105
134
|
composerContextVersion: 1,
|
|
135
|
+
scheduledTaskIdentityVersion: 1,
|
|
136
|
+
scheduledTaskCancellationVersion: 1,
|
|
106
137
|
parameterAgentVersion: 1,
|
|
138
|
+
directoryConcurrencyVersion: 1,
|
|
139
|
+
parameterInitializationVersion: 1,
|
|
107
140
|
remixReferencesVersion: 1,
|
|
108
141
|
batchSourceFreezeRevision: 2,
|
|
109
142
|
...(state.native?.workspaceUi ? { workspaceUiVersion: 1 } : {}),
|
|
110
143
|
...(state.native?.startReview && state.native?.steer ? { turnControlVersion: 1 } : {}),
|
|
111
144
|
...(state.native?.startCompact ? { compactionVersion: 1 } : {}),
|
|
145
|
+
...(state.native?.startPrediction ? { predictionVersion: 1 } : {}),
|
|
112
146
|
sourceReadToken: state.journal.sourceReadToken(),
|
|
113
147
|
workspaceMode: state.config.workspaceMode,
|
|
114
148
|
executionGroup: {
|
|
@@ -181,6 +215,27 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
181
215
|
writeJson(response, result.outcome === 'accepted' ? 200 : 409, result);
|
|
182
216
|
return;
|
|
183
217
|
}
|
|
218
|
+
const predictionRoute = /^\/runtime\/sessions\/([^/]+)\/prediction$/.exec(url.pathname);
|
|
219
|
+
if (request.method === 'POST' && predictionRoute) {
|
|
220
|
+
if (writeDestroyed(state, response))
|
|
221
|
+
return;
|
|
222
|
+
const sessionId = decodeURIComponent(predictionRoute[1]);
|
|
223
|
+
const session = state.journal.getSession(sessionId);
|
|
224
|
+
if (!session || productionSessionAttemptId(sessionId) || session.workspaceId !== url.searchParams.get('workspaceId')
|
|
225
|
+
|| (session.directoryName ?? 'basic') !== directoryName) {
|
|
226
|
+
writeJson(response, 404, { error: 'session_not_found' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!state.native?.startPrediction) {
|
|
230
|
+
writeJson(response, 409, { error: 'prediction_unavailable' });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Best-effort and read-only: no instruction/journal reservation, and the ephemeral
|
|
234
|
+
// fork never becomes a user-visible thread.
|
|
235
|
+
const result = await state.native.startPrediction({ sessionId, threadId: session.threadId });
|
|
236
|
+
writeJson(response, result.outcome === 'ok' ? 200 : 409, result);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
184
239
|
if (request.method === 'POST' && url.pathname === '/runtime/workspaces/register') {
|
|
185
240
|
if (writeDestroyed(state, response))
|
|
186
241
|
return;
|
|
@@ -189,7 +244,7 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
189
244
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
190
245
|
return;
|
|
191
246
|
}
|
|
192
|
-
const binding = await registerWorkspace(state, parsed.data.binding, parsed.data.leaseFence, parsed.data.preparation, parsed.data.cliCredentials === false, parsed.data.sourceSnapshot);
|
|
247
|
+
const binding = await withDirectoryWrite(state, { workspaceId: parsed.data.binding.workspaceId, directoryName: 'basic', wait: false }, () => registerWorkspace(state, parsed.data.binding, parsed.data.leaseFence, parsed.data.preparation, parsed.data.cliCredentials === false, parsed.data.sourceSnapshot));
|
|
193
248
|
if (parsed.data.cliCredentials)
|
|
194
249
|
await ensureCliCredentials(state);
|
|
195
250
|
writeJson(response, 200, { workspaceId: binding.workspaceId, status: 'registered' });
|
|
@@ -210,10 +265,13 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
210
265
|
writeJson(response, 400, { error: 'invalid_workspace_registration' });
|
|
211
266
|
return;
|
|
212
267
|
}
|
|
213
|
-
|
|
268
|
+
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
269
|
+
writeJson(response, 200, await withDirectoryWrite(state, { ...directory, mode: 'exclusive', wait: false }, () => withDirectoryWrite(state, { workspaceId: directory.workspaceId, directoryName: source.data, mode: 'exclusive', wait: false }, () => registerChatWorktree(state, directory, Number(body.leaseFence), source.data, body.sourceBranchName, attachments.data))));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
273
|
+
writeJson(response, 200, await withDirectoryWrite(state, { ...directory, mode: 'exclusive', wait: false }, () => registerWorkspaceDirectory(state, directory, Number(body.leaseFence))));
|
|
214
274
|
}
|
|
215
|
-
else
|
|
216
|
-
writeJson(response, 200, await registerWorkspaceDirectory(state, body.directory, Number(body.leaseFence)));
|
|
217
275
|
return;
|
|
218
276
|
}
|
|
219
277
|
if (request.method === 'POST' && url.pathname === '/runtime/workspaces/directories/worktree-source') {
|
|
@@ -226,7 +284,7 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
226
284
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
227
285
|
return;
|
|
228
286
|
}
|
|
229
|
-
writeJson(response, 200, await readWorktreeSource(state, body.workspaceId, source.data, Number(body.leaseFence), body.sourceBranchName));
|
|
287
|
+
writeJson(response, 200, await withDirectoryWrite(state, { workspaceId: body.workspaceId, directoryName: source.data, mode: 'exclusive', wait: false }, () => readWorktreeSource(state, body.workspaceId, source.data, Number(body.leaseFence), body.sourceBranchName)));
|
|
230
288
|
return;
|
|
231
289
|
}
|
|
232
290
|
const maintenance = /^\/runtime\/workspaces\/([a-f0-9-]{36})\/directories\/(cleanup|sync|stats)$/.exec(url.pathname);
|
|
@@ -763,6 +821,10 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
763
821
|
writeJson(response, 404, { error: 'not_found' });
|
|
764
822
|
}
|
|
765
823
|
catch (error) {
|
|
824
|
+
if (error instanceof Error && error.message === 'directory_busy') {
|
|
825
|
+
writeJson(response, 409, { error: 'directory_busy' });
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
766
828
|
if (request.method === 'GET' && ['/runtime/connectivity', '/runtime/files'].includes(url.pathname)) {
|
|
767
829
|
const cause = error instanceof Error && error.cause ? error.cause : error;
|
|
768
830
|
if (['EACCES', 'EPERM'].includes(String(cause?.code))) {
|
|
@@ -778,6 +840,12 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
778
840
|
writeJson(response, 413, { error: 'request_body_too_large' });
|
|
779
841
|
return;
|
|
780
842
|
}
|
|
843
|
+
// A shared path without development-group access is a repair, not an internal failure.
|
|
844
|
+
const drift = permissionDriftFromError(error);
|
|
845
|
+
if (drift) {
|
|
846
|
+
writeJson(response, 409, { error: 'workspace_permission_drift', ...drift });
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
781
849
|
if (error instanceof WorkspaceContextError) {
|
|
782
850
|
writeJson(response, 409, { error: error.message });
|
|
783
851
|
return;
|
|
@@ -858,7 +926,7 @@ async function acceptOperation(state, request, response) {
|
|
|
858
926
|
return;
|
|
859
927
|
}
|
|
860
928
|
// leaseFence 由 Runtime Server 派发,本控制面单独校验;其余代次/身份/根目录字段仍只来自引导配置。
|
|
861
|
-
const untrusted = findUntrustedRuntimeFields(body).filter((field) => field !== 'leaseFence');
|
|
929
|
+
const untrusted = findUntrustedRuntimeFields(body).filter((field) => field !== 'leaseFence' && field !== 'gatewayOperationId');
|
|
862
930
|
if (untrusted.length > 0) {
|
|
863
931
|
writeError(response, runtimeError('untrusted_runtime_field', untrusted.join(',')));
|
|
864
932
|
return;
|
|
@@ -896,7 +964,7 @@ async function acceptOperation(state, request, response) {
|
|
|
896
964
|
return;
|
|
897
965
|
}
|
|
898
966
|
if (state.config.workspaceMode === 'multi' && parsed.data.kind === 'runtime.ensure' && parsed.data.workspaceId)
|
|
899
|
-
await restoreWorkspaceDirectory(state, parsed.data.workspaceId, parsed.data.directoryName);
|
|
967
|
+
await withDirectoryWrite(state, { workspaceId: parsed.data.workspaceId, directoryName: parsed.data.directoryName, mode: 'exclusive', wait: false }, () => restoreWorkspaceDirectory(state, parsed.data.workspaceId, parsed.data.directoryName));
|
|
900
968
|
if (state.config.workspaceMode === 'multi')
|
|
901
969
|
await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
|
|
902
970
|
// Shared classification: a new conversation writes no files, so a batch hold must not refuse it.
|
|
@@ -995,7 +1063,9 @@ async function acceptOperation(state, request, response) {
|
|
|
995
1063
|
};
|
|
996
1064
|
// Directory maintenance uses this same lock before checking durable occupancy.
|
|
997
1065
|
if (occupiesDirectory && state.config.workspaceMode === 'multi')
|
|
998
|
-
await state.journal.exclusive(
|
|
1066
|
+
await state.journal.exclusive(parsed.data.kind.startsWith('session.') && parsed.data.sessionId
|
|
1067
|
+
? `session-admission:${parsed.data.sessionId}`
|
|
1068
|
+
: `directory:${parsed.data.workspaceId}:${parsed.data.directoryName ?? 'basic'}`, acceptBound);
|
|
999
1069
|
else
|
|
1000
1070
|
await acceptBound();
|
|
1001
1071
|
}
|
|
@@ -1152,7 +1222,7 @@ async function readObservedParameters(state, workspaceId, directoryName = 'basic
|
|
|
1152
1222
|
const scope = revisionScope(state, workspaceId, directoryName);
|
|
1153
1223
|
return state.journal.exclusive(`workspace:${scope ?? 'single'}`, async () => {
|
|
1154
1224
|
const first = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
|
|
1155
|
-
for (const path of [first.schemaPath, first.dataPath]) {
|
|
1225
|
+
for (const path of [first.schemaPath, first.dataPath, first.runtimeI18n?.globalPath, first.runtimeI18n?.themePath]) {
|
|
1156
1226
|
if (path)
|
|
1157
1227
|
await withObservedFile(state, workspaceId, path, async () => undefined, directoryName);
|
|
1158
1228
|
}
|
|
@@ -7,6 +7,8 @@ export function buildOperationRecord(state, parsed, payloadHash, leaseFence, exe
|
|
|
7
7
|
: {};
|
|
8
8
|
return {
|
|
9
9
|
operationId: randomUUID(),
|
|
10
|
+
...(parsed.gatewayOperationId ? { gatewayOperationId: parsed.gatewayOperationId } : {}),
|
|
11
|
+
...(parsed.kind === 'session.turn' && payload.parameterTask !== undefined ? { directoryAccessMode: 'exclusive' } : {}),
|
|
10
12
|
environment: state.config.environment,
|
|
11
13
|
userId: state.config.userId,
|
|
12
14
|
clientRequestId: parsed.clientRequestId,
|