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.
Files changed (65) hide show
  1. package/dist/mcp-capability-provider.js +4 -1
  2. package/dist/pi-tui-mcp-status.js +2 -2
  3. package/dist/pi-tui-pickers.js +19 -29
  4. package/dist/tui-autocomplete-layout.js +3 -1
  5. package/dist/tui-copy-catalog.js +6 -3
  6. package/dist/tui-editor-render.js +36 -0
  7. package/dist/tui-mcp-control.js +41 -6
  8. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  9. package/node_modules/@maka/core/dist/runtime-event.js +3 -0
  10. package/node_modules/@maka/core/dist/session.js +29 -4
  11. package/node_modules/@maka/core/dist/workhub-action-result.js +68 -0
  12. package/node_modules/@maka/core/package.json +2 -3
  13. package/node_modules/@maka/eval/dist/toolchain-verification.js +1 -1
  14. package/node_modules/@maka/mcp/dist/form-elicitation.js +213 -0
  15. package/node_modules/@maka/mcp/dist/index.js +337 -71
  16. package/node_modules/@maka/runtime/dist/agent-run.js +10 -1
  17. package/node_modules/@maka/runtime/dist/goal-continuation.js +5 -1
  18. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +20 -1
  19. package/node_modules/@maka/runtime/dist/runtime-kernel.js +120 -13
  20. package/node_modules/@maka/runtime/dist/session-manager.js +20 -36
  21. package/node_modules/@maka/runtime-host/dist/client/connection.js +30 -5
  22. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +17 -1
  23. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +4 -2
  24. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +18 -1
  25. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  26. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +3 -2
  27. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -0
  28. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +68 -168
  29. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -0
  30. package/node_modules/@maka/runtime-host/dist/server/canonical-session-projection.js +2 -1
  31. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +28 -8
  32. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +112 -56
  33. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +45 -20
  34. package/node_modules/@maka/runtime-host/dist/server/failure-diagnostic.js +24 -0
  35. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +7 -3
  36. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -3
  37. package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +5 -2
  38. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +3 -2
  39. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +34 -4
  40. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +4 -1
  41. package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +2 -7
  42. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +110 -14
  43. package/node_modules/@maka/runtime-host/dist/server/runtime-resource-coordinator.js +11 -8
  44. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +31 -21
  45. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +1 -1
  46. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +20 -8
  47. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +15 -104
  48. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +3 -0
  49. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -85
  50. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +120 -137
  51. package/node_modules/@maka/storage/dist/agent-run-store.js +14 -1
  52. package/node_modules/@maka/storage/dist/artifact-store.js +40 -3
  53. package/node_modules/@maka/storage/dist/atomic-file-write.js +74 -0
  54. package/node_modules/@maka/storage/dist/credential-store.js +12 -63
  55. package/node_modules/@maka/storage/dist/execution-stores.js +4 -1
  56. package/node_modules/@maka/storage/dist/mcp-config-store.js +10 -25
  57. package/node_modules/@maka/storage/dist/session-store.js +14 -2
  58. package/node_modules/@maka/storage/dist/settings-store.js +8 -4
  59. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +11 -0
  60. package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +12 -1
  61. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +67 -3
  62. package/node_modules/@maka/storage/dist/stable-storage.js +14 -1
  63. package/package.json +1 -1
  64. package/node_modules/@maka/core/dist/workhub-creation-intent.js +0 -990
  65. 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 { WORKHUB_COORDINATION_SUMMARY_MAX_BYTES, WORKHUB_COORDINATION_TEXT_MAX_BYTES, } from '../protocol/index.js';
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-v1';
33
- const SYNTHETIC_COORDINATION_MODEL_ID = 'maka-workhub-coordination';
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.act': (input, context) => this.#act(input, context),
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.actionId,
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.actionId,
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.actionId,
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 #act(input, context) {
330
+ async #actFromTurn(input, context) {
331
+ let content;
343
332
  try {
344
- return { ok: true, result: await this.#actionGate.act(input, context) };
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' ? 'session_busy' : 'operation_conflict',
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('workhub.coordination.answer', 'operation_conflict', 'WorkHub answer text is empty');
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
- // A recorded summary owns its Turn identity durably but is admitted
454
- // outside this ledger, so the probe runs under the admission lease: a
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
- #record(input) {
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 && validCoordinationHeader(record.header)) {
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
- ...(record.header.permissionMode === COORDINATION_PERMISSION_MODE
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 workspace could not be relocated');
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
- header.permissionMode === COORDINATION_PERMISSION_MODE &&
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(_operation, code, message) {
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, ['kind', 'inputDigest']) || !isSha256Digest(value.inputDigest)) {
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 target = join(this.artifactRoot, relativePath);
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(target);
265
- directories.add(dirname(target));
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
+ }