@gkorepanov/ccodex 0.4.5 → 0.4.7

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 (43) hide show
  1. package/README.md +31 -3
  2. package/config/compatibility.json +1 -1
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.js +4 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/delegate.js +1 -0
  7. package/dist/cli/delegate.js.map +1 -1
  8. package/dist/cli/main.js +21 -22
  9. package/dist/cli/main.js.map +1 -1
  10. package/dist/daemon/daemon.js +5 -4
  11. package/dist/daemon/daemon.js.map +1 -1
  12. package/dist/daemon/supervisor.d.ts +1 -0
  13. package/dist/daemon/supervisor.js +13 -3
  14. package/dist/daemon/supervisor.js.map +1 -1
  15. package/dist/desktop/launchAgent.d.ts +26 -0
  16. package/dist/desktop/launchAgent.js +122 -0
  17. package/dist/desktop/launchAgent.js.map +1 -0
  18. package/dist/desktop/stdioFrontend.d.ts +9 -0
  19. package/dist/desktop/stdioFrontend.js +305 -0
  20. package/dist/desktop/stdioFrontend.js.map +1 -0
  21. package/dist/gateway/clientConnection.js +2 -7
  22. package/dist/gateway/clientConnection.js.map +1 -1
  23. package/dist/handoff/service.d.ts +2 -2
  24. package/dist/handoff/service.js +158 -67
  25. package/dist/handoff/service.js.map +1 -1
  26. package/dist/handoff/store.d.ts +7 -4
  27. package/dist/handoff/store.js +55 -19
  28. package/dist/handoff/store.js.map +1 -1
  29. package/dist/management/doctor.js +31 -2
  30. package/dist/management/doctor.js.map +1 -1
  31. package/dist/management/lifecycle.js +80 -26
  32. package/dist/management/lifecycle.js.map +1 -1
  33. package/dist/management/setup.d.ts +3 -0
  34. package/dist/management/setup.js +75 -94
  35. package/dist/management/setup.js.map +1 -1
  36. package/dist/management/shellRouting.d.ts +14 -0
  37. package/dist/management/shellRouting.js +64 -0
  38. package/dist/management/shellRouting.js.map +1 -0
  39. package/dist/management/shims.d.ts +16 -3
  40. package/dist/management/shims.js +67 -12
  41. package/dist/management/shims.js.map +1 -1
  42. package/examples/config.toml +2 -0
  43. package/package.json +4 -4
@@ -287,6 +287,13 @@ export class CrossProviderForks {
287
287
  await this.deleteLogicalThread(publicThreadId, clientStock);
288
288
  return { provider: resolved.epoch.provider, result: {} };
289
289
  }
290
+ if (method === "thread/archive") {
291
+ this.store.cancelProviderSwitches(publicThreadId, "Task archived.");
292
+ if (resolved.epoch.backendThreadId.startsWith("ccodex-provisional:")) {
293
+ await this.deleteLogicalThread(publicThreadId, clientStock);
294
+ return { provider: resolved.epoch.provider, result: {} };
295
+ }
296
+ }
290
297
  if (resolved.epoch.backendThreadId.startsWith("ccodex-provisional:")) {
291
298
  return this.requestProvisionalLogical(method, publicParams, resolved);
292
299
  }
@@ -523,9 +530,12 @@ export class CrossProviderForks {
523
530
  this.patchLogicalThread(resolved.logical.publicThreadId, { name: params.name });
524
531
  return { provider: resolved.epoch.provider, result: {} };
525
532
  }
533
+ if (method === "thread/goal/get") {
534
+ return { provider: resolved.epoch.provider, result: { goal: null } };
535
+ }
526
536
  throw invalidParams("This fork is waiting for Codex App to select a completed history boundary.");
527
537
  }
528
- async forkLogical(params, clientStock) {
538
+ async forkLogical(params, clientStock, connectionId) {
529
539
  const source = this.epochs.resolve(params.threadId);
530
540
  if (!source)
531
541
  throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
@@ -534,11 +544,54 @@ export class CrossProviderForks {
534
544
  }
535
545
  const currentTurns = await this.currentBackendTurns(source, this.daemonStock ?? clientStock);
536
546
  const visible = this.epochs.snapshotTurns(params.threadId, currentTurns);
547
+ const boundaryIndex = visible.findLastIndex((turn) => turn.turn.status === "completed" && turn.epochId && turn.providerTurnId);
548
+ const boundary = visible[boundaryIndex];
549
+ if (!boundary?.epochId || !boundary.providerTurnId) {
550
+ throw invalidParams("Cannot fork before the task has a completed provider turn.");
551
+ }
552
+ const selectedEpoch = this.store.getEpoch(boundary.epochId);
553
+ if (!selectedEpoch || !this.store.epochBelongsToLineage(params.threadId, selectedEpoch.id)) {
554
+ throw invalidParams("Fork boundary is outside the task's provider lineage.");
555
+ }
537
556
  const targetId = uuidv7();
538
- const placeholderId = `ccodex-provisional:${uuidv7()}`;
557
+ let forked;
558
+ let stockBuildOwner;
559
+ if (selectedEpoch.provider === "claude") {
560
+ forked = await this.claude.forkThread({
561
+ threadId: selectedEpoch.backendThreadId,
562
+ lastTurnId: boundary.providerTurnId,
563
+ model: selectedEpoch.model,
564
+ }, params.threadId);
565
+ }
566
+ else {
567
+ stockBuildOwner = connectionId;
568
+ if (stockBuildOwner) {
569
+ if (this.stockTargetBuilds.has(stockBuildOwner)) {
570
+ throw new Error("Another stock thread materialization is already active on this connection.");
571
+ }
572
+ this.stockTargetBuilds.set(stockBuildOwner, {
573
+ awaitingStart: true,
574
+ threadIds: new Set(),
575
+ messages: [],
576
+ expectedForkedFromId: selectedEpoch.backendThreadId,
577
+ });
578
+ }
579
+ try {
580
+ forked = await clientStock.request("thread/fork", {
581
+ threadId: selectedEpoch.backendThreadId,
582
+ lastTurnId: boundary.providerTurnId,
583
+ model: selectedEpoch.model,
584
+ });
585
+ }
586
+ catch (error) {
587
+ if (stockBuildOwner)
588
+ this.stockTargetBuilds.delete(stockBuildOwner);
589
+ throw error;
590
+ }
591
+ }
539
592
  const now = Math.floor(Date.now() / 1_000);
540
593
  const thread = {
541
- ...source.logical.thread,
594
+ ...forked.thread,
542
595
  id: targetId,
543
596
  sessionId: source.logical.thread.sessionId,
544
597
  forkedFromId: params.threadId,
@@ -548,67 +601,102 @@ export class CrossProviderForks {
548
601
  recencyAt: now,
549
602
  status: { type: "notLoaded" },
550
603
  name: source.logical.thread.name ? `${source.logical.thread.name} (fork)` : null,
551
- turns: visible.map((turn) => turn.turn),
552
- };
553
- const target = this.store.createLogicalThread({
554
- thread,
555
- epoch: {
556
- id: uuidv7(),
557
- provider: source.epoch.provider,
558
- backendThreadId: placeholderId,
559
- model: source.epoch.model,
560
- settings: source.epoch.settings,
561
- },
562
- });
563
- if (visible.length > 0 && !this.store.replaceLogicalTurns(targetId, target.revision, visible)) {
564
- throw new Error("Failed to persist provisional logical fork history.");
565
- }
566
- const provisional = this.epochs.resolve(targetId);
567
- if (!this.store.createForkSelection({
568
- targetPublicThreadId: targetId,
569
- sourcePublicThreadId: params.threadId,
570
- provisionalEpochId: provisional.epoch.id,
571
- }))
572
- throw new Error("Failed to persist logical fork selection.");
573
- const settings = source.epoch.settings;
574
- const response = {
575
- thread: { ...thread, turns: params.excludeTurns ? [] : thread.turns },
576
- model: source.epoch.model,
577
- modelProvider: source.epoch.provider === "claude" ? "claude" : "openai",
578
- serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
579
- cwd: thread.cwd,
580
- runtimeWorkspaceRoots: Array.isArray(settings.runtimeWorkspaceRoots)
581
- ? settings.runtimeWorkspaceRoots : [],
582
- instructionSources: Array.isArray(settings.instructionSources)
583
- ? settings.instructionSources : [],
584
- approvalPolicy: (settings.approvalPolicy ?? "on-request"),
585
- approvalsReviewer: (settings.approvalsReviewer ?? "user"),
586
- sandbox: (settings.sandbox ?? settings.sandboxPolicy ?? { type: "readOnly" }),
587
- activePermissionProfile: (settings.activePermissionProfile ?? null),
588
- reasoningEffort: (settings.reasoningEffort ?? settings.effort ?? null),
589
- multiAgentMode: "explicitRequestOnly",
604
+ turns: [],
590
605
  };
591
- this.subscriptions?.emit(targetId, "thread/started", { thread: { ...thread, turns: [] } });
592
- return response;
606
+ try {
607
+ const target = this.store.createLogicalThread({
608
+ thread,
609
+ epoch: {
610
+ id: uuidv7(),
611
+ provider: selectedEpoch.provider,
612
+ backendThreadId: forked.thread.id,
613
+ model: selectedEpoch.model,
614
+ settings: selectedEpoch.settings,
615
+ },
616
+ });
617
+ const inherited = visible.slice(0, boundaryIndex + 1).filter((turn) => turn.epochId !== selectedEpoch.id);
618
+ if (inherited.length > 0 && !this.store.replaceLogicalTurns(targetId, target.revision, inherited)) {
619
+ throw new Error("Failed to persist logical fork history.");
620
+ }
621
+ this.subscriptions?.aliasThread(forked.thread.id, targetId);
622
+ if (stockBuildOwner)
623
+ this.stockTargetBuilds.delete(stockBuildOwner);
624
+ if (selectedEpoch.provider === "claude") {
625
+ await this.claude.announceThread(forked.thread);
626
+ }
627
+ else {
628
+ this.subscriptions?.emitPublic(targetId, "thread/started", {
629
+ thread: this.epochs.projectThread(targetId, forked.thread, false),
630
+ });
631
+ }
632
+ const settings = selectedEpoch.settings;
633
+ return {
634
+ ...forked,
635
+ thread: this.epochs.projectThread(targetId, forked.thread, !params.excludeTurns),
636
+ model: selectedEpoch.model,
637
+ modelProvider: selectedEpoch.provider === "claude" ? "claude" : "openai",
638
+ serviceTier: typeof settings.serviceTier === "string" ? settings.serviceTier : null,
639
+ };
640
+ }
641
+ catch (error) {
642
+ if (stockBuildOwner)
643
+ this.stockTargetBuilds.delete(stockBuildOwner);
644
+ this.store.deleteLogicalThread(targetId);
645
+ if (!forked?.thread.id)
646
+ throw error;
647
+ if (selectedEpoch.provider === "claude") {
648
+ await this.claude.deleteThread(forked.thread.id).catch(() => undefined);
649
+ }
650
+ else {
651
+ await clientStock.request("thread/delete", { threadId: forked.thread.id }).catch(() => undefined);
652
+ }
653
+ throw error;
654
+ }
593
655
  }
594
- async rollbackLogicalFork(params, clientStock, connectionId) {
656
+ async rollbackLogicalThread(params, clientStock, connectionId) {
595
657
  const selection = this.store.getForkSelection(params.threadId);
596
658
  const target = this.epochs.resolve(params.threadId);
597
- if (!selection || selection.status !== "pending" || !target) {
598
- throw invalidParams(`Thread '${params.threadId}' is not awaiting a logical fork selection.`);
659
+ if (!target)
660
+ throw invalidParams(`Unknown logical thread '${params.threadId}'.`);
661
+ if (!Number.isInteger(params.numTurns) || params.numTurns < 0) {
662
+ throw invalidParams("numTurns must be a non-negative integer.");
599
663
  }
600
- const turns = this.store.listLogicalTurns(params.threadId);
664
+ const pendingFork = selection?.status === "pending" ? selection : undefined;
665
+ const currentTurns = await this.currentBackendTurns(target, this.daemonStock ?? clientStock);
666
+ const turns = this.epochs.snapshotTurns(params.threadId, currentTurns);
601
667
  const keep = turns.length - params.numTurns;
602
668
  if (keep <= 0)
603
- throw invalidParams("Fork rollback removed every provider turn.");
669
+ throw invalidParams("Rollback removed every provider turn.");
604
670
  const retained = turns.slice(0, keep);
605
671
  const boundary = [...retained].reverse().find((turn) => turn.epochId && turn.providerTurnId);
606
672
  if (!boundary?.epochId || !boundary.providerTurnId) {
607
- throw invalidParams("Fork selection has no provider-backed turn boundary.");
673
+ throw invalidParams("Rollback has no provider-backed turn boundary.");
608
674
  }
609
675
  const selectedEpoch = this.store.getEpoch(boundary.epochId);
610
- if (!selectedEpoch || selectedEpoch.publicThreadId !== selection.sourcePublicThreadId) {
611
- throw invalidParams("Fork selection points outside the source provider lineage.");
676
+ const selectedLineage = pendingFork?.sourcePublicThreadId ?? params.threadId;
677
+ if (!selectedEpoch || !this.store.epochBelongsToLineage(selectedLineage, selectedEpoch.id)) {
678
+ throw invalidParams("Rollback points outside the thread's provider lineage.");
679
+ }
680
+ if (!pendingFork && selectedEpoch.id === target.epoch.id) {
681
+ if (params.numTurns === 0) {
682
+ const backend = target.epoch.provider === "claude"
683
+ ? this.claude.readThread(target.epoch.backendThreadId, true).thread
684
+ : (await clientStock.request("thread/read", {
685
+ threadId: target.epoch.backendThreadId,
686
+ includeTurns: true,
687
+ })).thread;
688
+ return { thread: this.epochs.projectThread(params.threadId, backend, true) };
689
+ }
690
+ const rolled = target.epoch.provider === "claude"
691
+ ? await this.claude.rollbackThread({
692
+ threadId: target.epoch.backendThreadId,
693
+ numTurns: params.numTurns,
694
+ })
695
+ : await clientStock.request("thread/rollback", {
696
+ threadId: target.epoch.backendThreadId,
697
+ numTurns: params.numTurns,
698
+ });
699
+ return { thread: this.epochs.projectThread(params.threadId, rolled.thread, true) };
612
700
  }
613
701
  let forked;
614
702
  let stockBuildOwner;
@@ -649,16 +737,17 @@ export class CrossProviderForks {
649
737
  ...forked.thread,
650
738
  id: params.threadId,
651
739
  sessionId: target.logical.thread.sessionId,
652
- forkedFromId: selection.sourcePublicThreadId,
653
- parentThreadId: null,
740
+ forkedFromId: target.logical.thread.forkedFromId,
741
+ parentThreadId: target.logical.thread.parentThreadId,
654
742
  createdAt: target.logical.thread.createdAt,
655
743
  name: target.logical.thread.name,
656
744
  turns: [],
657
745
  };
658
- const committed = this.store.commitForkSelection({
746
+ const committed = this.store.commitLogicalRollback({
659
747
  targetPublicThreadId: params.threadId,
660
- expectedProvisionalEpochId: selection.provisionalEpochId,
661
- selectedSourceEpochId: selectedEpoch.id,
748
+ expectedCurrentEpochId: target.epoch.id,
749
+ expectedThreadRevision: target.logical.revision,
750
+ selectedEpochId: selectedEpoch.id,
662
751
  targetEpoch: {
663
752
  id: uuidv7(),
664
753
  provider: selectedEpoch.provider,
@@ -678,18 +767,20 @@ export class CrossProviderForks {
678
767
  if (!committed) {
679
768
  if (stockBuildOwner)
680
769
  this.stockTargetBuilds.delete(stockBuildOwner);
681
- throw new Error("Logical fork selection lost its atomic commit boundary.");
770
+ throw new Error("Logical rollback lost its atomic commit boundary.");
682
771
  }
683
772
  this.subscriptions?.suppress(target.epoch.backendThreadId);
684
773
  this.subscriptions?.aliasThread(forked.thread.id, params.threadId);
685
774
  if (stockBuildOwner)
686
775
  this.stockTargetBuilds.delete(stockBuildOwner);
687
- if (selectedEpoch.provider === "claude")
688
- await this.claude.announceThread(forked.thread);
689
- else
690
- this.subscriptions?.emitPublic(params.threadId, "thread/started", {
691
- thread: this.epochs.projectThread(params.threadId, forked.thread, true),
692
- });
776
+ if (pendingFork) {
777
+ if (selectedEpoch.provider === "claude")
778
+ await this.claude.announceThread(forked.thread);
779
+ else
780
+ this.subscriptions?.emitPublic(params.threadId, "thread/started", {
781
+ thread: this.epochs.projectThread(params.threadId, forked.thread, true),
782
+ });
783
+ }
693
784
  this.subscriptions?.emitPublic(params.threadId, "thread/settings/updated", {
694
785
  threadId: params.threadId,
695
786
  threadSettings: {
@@ -1335,20 +1426,20 @@ export class CrossProviderForks {
1335
1426
  }
1336
1427
  async deleteLogicalThread(publicThreadId, clientStock) {
1337
1428
  const stock = this.daemonStock ?? clientStock;
1429
+ this.store.cancelProviderSwitches(publicThreadId, "Task deleted.");
1338
1430
  const mappings = this.store.listBackendMappings()
1339
1431
  .filter((mapping) => mapping.publicThreadId === publicThreadId
1340
1432
  && !mapping.backendThreadId.startsWith("ccodex-provisional:"));
1341
1433
  for (const mapping of mappings)
1342
1434
  this.subscriptions?.suppress(mapping.backendThreadId);
1435
+ this.store.deleteLogicalThread(publicThreadId);
1436
+ this.subscriptions?.threadDeleted(publicThreadId);
1343
1437
  await Promise.allSettled(mappings.map((mapping) => mapping.provider === "claude"
1344
1438
  ? this.claude.deleteThread(mapping.backendThreadId)
1345
1439
  : stock.request("thread/delete", { threadId: mapping.backendThreadId })));
1346
1440
  for (const mapping of mappings) {
1347
1441
  this.subscriptions?.unaliasThread(mapping.backendThreadId);
1348
1442
  }
1349
- this.store.clearPending(publicThreadId);
1350
- this.store.deleteLogicalThread(publicThreadId);
1351
- this.subscriptions?.threadDeleted(publicThreadId);
1352
1443
  }
1353
1444
  async providerSwitchSource(job, stock, _connectionId) {
1354
1445
  const resolved = this.epochs.resolve(job.publicThreadId);