@agent-relay/factory 0.1.62 → 0.1.64

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 (166) hide show
  1. package/README.md +117 -15
  2. package/dist/cli/exit-codes.d.ts +81 -0
  3. package/dist/cli/exit-codes.d.ts.map +1 -0
  4. package/dist/cli/exit-codes.js +122 -0
  5. package/dist/cli/exit-codes.js.map +1 -0
  6. package/dist/cli/fleet.d.ts +26 -2
  7. package/dist/cli/fleet.d.ts.map +1 -1
  8. package/dist/cli/fleet.js +183 -40
  9. package/dist/cli/fleet.js.map +1 -1
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +2 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/config/schema.d.ts +156 -52
  15. package/dist/config/schema.d.ts.map +1 -1
  16. package/dist/config/schema.js +17 -5
  17. package/dist/config/schema.js.map +1 -1
  18. package/dist/dispatch/templates.d.ts +7 -0
  19. package/dist/dispatch/templates.d.ts.map +1 -1
  20. package/dist/dispatch/templates.js +64 -12
  21. package/dist/dispatch/templates.js.map +1 -1
  22. package/dist/dispatch/work-unit-identity.d.ts +19 -0
  23. package/dist/dispatch/work-unit-identity.d.ts.map +1 -0
  24. package/dist/dispatch/work-unit-identity.js +40 -0
  25. package/dist/dispatch/work-unit-identity.js.map +1 -0
  26. package/dist/environments/stack-descriptor.d.ts +3 -3
  27. package/dist/fleet/control-plane-circuit.d.ts +57 -0
  28. package/dist/fleet/control-plane-circuit.d.ts.map +1 -0
  29. package/dist/fleet/control-plane-circuit.js +231 -0
  30. package/dist/fleet/control-plane-circuit.js.map +1 -0
  31. package/dist/fleet/internal-fleet-client.d.ts +2 -1
  32. package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
  33. package/dist/fleet/internal-fleet-client.js +11 -7
  34. package/dist/fleet/internal-fleet-client.js.map +1 -1
  35. package/dist/fleet/relay-fleet-client.d.ts +3 -0
  36. package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
  37. package/dist/fleet/relay-fleet-client.js +97 -18
  38. package/dist/fleet/relay-fleet-client.js.map +1 -1
  39. package/dist/github/writeback-paths.d.ts +7 -0
  40. package/dist/github/writeback-paths.d.ts.map +1 -0
  41. package/dist/github/writeback-paths.js +28 -0
  42. package/dist/github/writeback-paths.js.map +1 -0
  43. package/dist/hosted/orchestrator.d.ts.map +1 -1
  44. package/dist/hosted/orchestrator.js +2 -0
  45. package/dist/hosted/orchestrator.js.map +1 -1
  46. package/dist/index.d.ts +10 -4
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +6 -3
  49. package/dist/index.js.map +1 -1
  50. package/dist/intake/index.d.ts +2 -1
  51. package/dist/intake/index.d.ts.map +1 -1
  52. package/dist/intake/index.js +2 -1
  53. package/dist/intake/index.js.map +1 -1
  54. package/dist/intake/notion-manifest.d.ts +48 -0
  55. package/dist/intake/notion-manifest.d.ts.map +1 -0
  56. package/dist/intake/notion-manifest.js +258 -0
  57. package/dist/intake/notion-manifest.js.map +1 -0
  58. package/dist/intake/notion-relay-claim.d.ts +5 -4
  59. package/dist/intake/notion-relay-claim.d.ts.map +1 -1
  60. package/dist/intake/notion-relay-claim.js +48 -7
  61. package/dist/intake/notion-relay-claim.js.map +1 -1
  62. package/dist/intake/notion.d.ts +6 -3
  63. package/dist/intake/notion.d.ts.map +1 -1
  64. package/dist/intake/notion.js +63 -13
  65. package/dist/intake/notion.js.map +1 -1
  66. package/dist/issue-key-match.d.ts +6 -0
  67. package/dist/issue-key-match.d.ts.map +1 -1
  68. package/dist/issue-key-match.js +6 -1
  69. package/dist/issue-key-match.js.map +1 -1
  70. package/dist/mount/local-mount-preflight.d.ts.map +1 -1
  71. package/dist/mount/local-mount-preflight.js +6 -2
  72. package/dist/mount/local-mount-preflight.js.map +1 -1
  73. package/dist/mount/mount-auth-error.d.ts +16 -3
  74. package/dist/mount/mount-auth-error.d.ts.map +1 -1
  75. package/dist/mount/mount-auth-error.js +34 -7
  76. package/dist/mount/mount-auth-error.js.map +1 -1
  77. package/dist/mount/relayfile-cloud-mount-client.d.ts +36 -1
  78. package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
  79. package/dist/mount/relayfile-cloud-mount-client.js +136 -6
  80. package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
  81. package/dist/mount/relayfile-github-connection-write.d.ts +24 -2
  82. package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
  83. package/dist/mount/relayfile-github-connection-write.js +90 -1
  84. package/dist/mount/relayfile-github-connection-write.js.map +1 -1
  85. package/dist/node/factory-node.d.ts.map +1 -1
  86. package/dist/node/factory-node.js +18 -4
  87. package/dist/node/factory-node.js.map +1 -1
  88. package/dist/observability/cloud-reporter.d.ts +1 -0
  89. package/dist/observability/cloud-reporter.d.ts.map +1 -1
  90. package/dist/observability/cloud-reporter.js +182 -50
  91. package/dist/observability/cloud-reporter.js.map +1 -1
  92. package/dist/observability/events.d.ts +52 -52
  93. package/dist/orchestrator/batch-tracker.d.ts +25 -0
  94. package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
  95. package/dist/orchestrator/batch-tracker.js +48 -2
  96. package/dist/orchestrator/batch-tracker.js.map +1 -1
  97. package/dist/orchestrator/factory.d.ts +36 -2
  98. package/dist/orchestrator/factory.d.ts.map +1 -1
  99. package/dist/orchestrator/factory.js +1208 -82
  100. package/dist/orchestrator/factory.js.map +1 -1
  101. package/dist/orchestrator/index.d.ts +1 -1
  102. package/dist/orchestrator/index.d.ts.map +1 -1
  103. package/dist/orchestrator/index.js +1 -1
  104. package/dist/orchestrator/index.js.map +1 -1
  105. package/dist/ports/fleet.d.ts +6 -0
  106. package/dist/ports/fleet.d.ts.map +1 -1
  107. package/dist/ports/index.d.ts +1 -1
  108. package/dist/ports/index.d.ts.map +1 -1
  109. package/dist/ports/mount.d.ts +39 -0
  110. package/dist/ports/mount.d.ts.map +1 -1
  111. package/dist/ports/state.d.ts +75 -4
  112. package/dist/ports/state.d.ts.map +1 -1
  113. package/dist/state/document-store.d.ts +33 -0
  114. package/dist/state/document-store.d.ts.map +1 -0
  115. package/dist/state/document-store.js +7 -0
  116. package/dist/state/document-store.js.map +1 -0
  117. package/dist/state/file-state-store.d.ts +32 -7
  118. package/dist/state/file-state-store.d.ts.map +1 -1
  119. package/dist/state/file-state-store.js +169 -250
  120. package/dist/state/file-state-store.js.map +1 -1
  121. package/dist/state/in-memory-state-store.d.ts +13 -2
  122. package/dist/state/in-memory-state-store.d.ts.map +1 -1
  123. package/dist/state/in-memory-state-store.js +87 -4
  124. package/dist/state/in-memory-state-store.js.map +1 -1
  125. package/dist/state/watch-state-document.d.ts +5 -0
  126. package/dist/state/watch-state-document.d.ts.map +1 -0
  127. package/dist/state/watch-state-document.js +486 -0
  128. package/dist/state/watch-state-document.js.map +1 -0
  129. package/dist/testing/deadline.d.ts +11 -0
  130. package/dist/testing/deadline.d.ts.map +1 -0
  131. package/dist/testing/deadline.js +23 -0
  132. package/dist/testing/deadline.js.map +1 -0
  133. package/dist/testing/fakes.d.ts +2 -0
  134. package/dist/testing/fakes.d.ts.map +1 -1
  135. package/dist/testing/fakes.js.map +1 -1
  136. package/dist/testing/index.d.ts +1 -0
  137. package/dist/testing/index.d.ts.map +1 -1
  138. package/dist/testing/index.js +1 -0
  139. package/dist/testing/index.js.map +1 -1
  140. package/dist/trajectory.d.ts +25 -0
  141. package/dist/trajectory.d.ts.map +1 -0
  142. package/dist/trajectory.js +51 -0
  143. package/dist/trajectory.js.map +1 -0
  144. package/dist/triage/heuristic.d.ts +5 -0
  145. package/dist/triage/heuristic.d.ts.map +1 -1
  146. package/dist/triage/heuristic.js +42 -3
  147. package/dist/triage/heuristic.js.map +1 -1
  148. package/dist/triage/index.d.ts +1 -1
  149. package/dist/triage/index.d.ts.map +1 -1
  150. package/dist/triage/index.js +1 -1
  151. package/dist/triage/index.js.map +1 -1
  152. package/dist/triage/schema.d.ts +73 -55
  153. package/dist/triage/schema.d.ts.map +1 -1
  154. package/dist/triage/schema.js +2 -1
  155. package/dist/triage/schema.js.map +1 -1
  156. package/dist/types.d.ts +19 -3
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/writeback/github.d.ts +22 -7
  159. package/dist/writeback/github.d.ts.map +1 -1
  160. package/dist/writeback/github.js +86 -16
  161. package/dist/writeback/github.js.map +1 -1
  162. package/dist/writeback/index.d.ts +1 -1
  163. package/dist/writeback/index.d.ts.map +1 -1
  164. package/dist/writeback/index.js +1 -1
  165. package/dist/writeback/index.js.map +1 -1
  166. package/package.json +13 -8
@@ -5,6 +5,7 @@ import lockfile from 'proper-lockfile';
5
5
  import { githubRepositoriesMatch } from '../github/repo-identity.js';
6
6
  import { InMemoryStateStore } from './in-memory-state-store.js';
7
7
  import { matchingGithubLifecycleEntry } from './github-lifecycle-identity.js';
8
+ import { emptyDiscoverySweepState, parseWatchStateDocument } from './watch-state-document.js';
8
9
  export const githubWatchStatePath = (registryPath) => join(dirname(registryPath), 'github-issue-comment-watches.json');
9
10
  // proper-lockfile refreshes a live writer's lease at half this interval. If a
10
11
  // process crashes, the next writer can reclaim its lock after this TTL. The
@@ -14,23 +15,28 @@ export const githubWatchStatePath = (registryPath) => join(dirname(registryPath)
14
15
  const WATCH_STATE_LOCK_STALE_MS = 60_000;
15
16
  /**
16
17
  * Keeps the factory's general runtime bookkeeping in memory while persisting
17
- * GitHub escalation watches, parked clarification teams, exact babysitter PR
18
- * ownership, and thread-owned conversation turns atomically so they survive a
19
- * CLI process restart.
18
+ * GitHub/Slack escalation watches, parked clarification teams, exact
19
+ * babysitter PR ownership, and thread-owned conversation turns atomically so
20
+ * they survive a CLI process restart.
20
21
  * Mutations reload under an advisory lock so independent processes merge
21
22
  * updates instead of publishing divergent cached documents.
22
23
  */
23
- export class FileStateStore extends InMemoryStateStore {
24
- #watchStatePath;
24
+ export class DocumentStateStore extends InMemoryStateStore {
25
+ backend;
26
+ #documentStore;
25
27
  #batchSize;
26
28
  #isProcessAlive;
27
29
  #operation = Promise.resolve();
28
30
  constructor(options) {
29
31
  super(options);
30
- this.#watchStatePath = options.watchStatePath;
32
+ this.backend = options.backend;
33
+ this.#documentStore = options.documentStore;
31
34
  this.#batchSize = options.batchSize;
32
35
  this.#isProcessAlive = options.isProcessAlive ?? processIsAlive;
33
36
  }
37
+ async assertReady() {
38
+ await this.#exclusive(async () => this.#documentStore.assertReady());
39
+ }
34
40
  async claimDiscoverySweep(workspaceId, owner, nowMs, leaseMs) {
35
41
  return await this.#exclusive(async () => this.#withMutationLock(async () => {
36
42
  const document = await this.#loadFromDisk();
@@ -256,6 +262,33 @@ export class FileStateStore extends InMemoryStateStore {
256
262
  await this.#persist(document);
257
263
  }));
258
264
  }
265
+ async setSlackThreadWatch(workspaceId, key, watch) {
266
+ await this.#exclusive(async () => this.#withMutationLock(async () => {
267
+ const document = await this.#loadFromDisk();
268
+ const workspace = document.workspaces[workspaceId] ??= emptyWorkspaceState();
269
+ workspace.slackThreadWatches[key] = structuredClone(watch);
270
+ await this.#persist(document);
271
+ }));
272
+ }
273
+ async listSlackThreadWatches(workspaceId) {
274
+ return await this.#exclusive(async () => {
275
+ const document = await this.#loadFromDisk();
276
+ return Object.entries(document.workspaces[workspaceId]?.slackThreadWatches ?? {})
277
+ .map(([key, watch]) => [key, structuredClone(watch)]);
278
+ });
279
+ }
280
+ async clearSlackThreadWatch(workspaceId, key) {
281
+ await this.#exclusive(async () => this.#withMutationLock(async () => {
282
+ const document = await this.#loadFromDisk();
283
+ const workspace = document.workspaces[workspaceId];
284
+ if (!workspace || !(key in workspace.slackThreadWatches))
285
+ return;
286
+ delete workspace.slackThreadWatches[key];
287
+ if (workspaceIsEmpty(workspace))
288
+ delete document.workspaces[workspaceId];
289
+ await this.#persist(document);
290
+ }));
291
+ }
259
292
  async setGithubIssueCommentWatch(workspaceId, key, watch) {
260
293
  await this.#exclusive(async () => {
261
294
  await this.#withMutationLock(async () => {
@@ -671,6 +704,90 @@ export class FileStateStore extends InMemoryStateStore {
671
704
  return true;
672
705
  });
673
706
  }
707
+ async claimConversationMessageAcknowledgement(workspaceId, conversationId, messageId, claimId, nowMs, leaseMs) {
708
+ const result = await this.#mutateConversation(workspaceId, conversationId, (session) => {
709
+ if (!conversationHasMessage(session, messageId))
710
+ return false;
711
+ if ((session.acknowledgedMessageIds ?? []).includes(messageId))
712
+ return false;
713
+ session.acknowledgementClaims ??= {};
714
+ const current = session.acknowledgementClaims[messageId];
715
+ if (current && current.claimedAtMs + leaseMs > nowMs)
716
+ return false;
717
+ session.acknowledgementClaims[messageId] = { claimId, claimedAtMs: nowMs };
718
+ return true;
719
+ });
720
+ return Boolean(result);
721
+ }
722
+ async completeConversationMessageAcknowledgement(workspaceId, conversationId, messageId, claimId) {
723
+ const result = await this.#mutateConversation(workspaceId, conversationId, (session) => {
724
+ if (session.acknowledgementClaims?.[messageId]?.claimId !== claimId)
725
+ return false;
726
+ session.acknowledgedMessageIds ??= [];
727
+ if (!session.acknowledgedMessageIds.includes(messageId))
728
+ session.acknowledgedMessageIds.push(messageId);
729
+ delete session.acknowledgementClaims[messageId];
730
+ return true;
731
+ });
732
+ return Boolean(result);
733
+ }
734
+ async renewConversationMessageAcknowledgement(workspaceId, conversationId, messageId, claimId, nowMs) {
735
+ const result = await this.#mutateConversation(workspaceId, conversationId, (session) => {
736
+ const claim = session.acknowledgementClaims?.[messageId];
737
+ if (claim?.claimId !== claimId)
738
+ return false;
739
+ claim.claimedAtMs = nowMs;
740
+ return true;
741
+ });
742
+ return Boolean(result);
743
+ }
744
+ async releaseConversationMessageAcknowledgement(workspaceId, conversationId, messageId, claimId) {
745
+ await this.#mutateConversation(workspaceId, conversationId, (session) => {
746
+ if (session.acknowledgementClaims?.[messageId]?.claimId !== claimId)
747
+ return false;
748
+ delete session.acknowledgementClaims[messageId];
749
+ return true;
750
+ });
751
+ }
752
+ async claimConversationTerminalReceipt(workspaceId, conversationId, claimId, nowMs, leaseMs) {
753
+ const result = await this.#mutateConversation(workspaceId, conversationId, (session) => {
754
+ if (session.terminalReceipt?.posted)
755
+ return false;
756
+ const current = session.terminalReceipt;
757
+ if (current && current.claimedAtMs + leaseMs > nowMs)
758
+ return false;
759
+ session.terminalReceipt = { claimId, claimedAtMs: nowMs };
760
+ return true;
761
+ });
762
+ return Boolean(result);
763
+ }
764
+ async renewConversationTerminalReceipt(workspaceId, conversationId, claimId, nowMs) {
765
+ const result = await this.#mutateConversation(workspaceId, conversationId, (session) => {
766
+ const receipt = session.terminalReceipt;
767
+ if (receipt?.claimId !== claimId || receipt.posted)
768
+ return false;
769
+ receipt.claimedAtMs = nowMs;
770
+ return true;
771
+ });
772
+ return Boolean(result);
773
+ }
774
+ async completeConversationTerminalReceipt(workspaceId, conversationId, claimId) {
775
+ const result = await this.#mutateConversation(workspaceId, conversationId, (session) => {
776
+ if (session.terminalReceipt?.claimId !== claimId)
777
+ return false;
778
+ session.terminalReceipt = { ...session.terminalReceipt, posted: true };
779
+ return true;
780
+ });
781
+ return Boolean(result);
782
+ }
783
+ async releaseConversationTerminalReceipt(workspaceId, conversationId, claimId) {
784
+ await this.#mutateConversation(workspaceId, conversationId, (session) => {
785
+ if (session.terminalReceipt?.claimId !== claimId || session.terminalReceipt.posted)
786
+ return false;
787
+ delete session.terminalReceipt;
788
+ return true;
789
+ });
790
+ }
674
791
  async claimConversationTurn(workspaceId, conversationId, owner, claimId, nowMs, leaseMs) {
675
792
  return await this.#mutateConversation(workspaceId, conversationId, (session) => {
676
793
  if (session.delivery && session.delivery.claimedAtMs + leaseMs > nowMs) {
@@ -680,10 +797,12 @@ export class FileStateStore extends InMemoryStateStore {
680
797
  if (session.delivery)
681
798
  session.pending.unshift(...session.delivery.messages);
682
799
  session.pending.sort(compareConversationMessages);
683
- if (session.pending.length === 0) {
800
+ if (!session.agent || session.pending.length === 0) {
801
+ const hadDelivery = session.delivery !== undefined;
684
802
  session.delivery = undefined;
685
- return false;
803
+ return hadDelivery;
686
804
  }
805
+ const agent = session.agent;
687
806
  session.delivery = {
688
807
  claimId,
689
808
  owner,
@@ -691,8 +810,8 @@ export class FileStateStore extends InMemoryStateStore {
691
810
  attempts: attempts + 1,
692
811
  messages: session.pending.splice(0),
693
812
  agent: {
694
- name: session.agent.name,
695
- sessionRef: session.agent.sessionRef,
813
+ name: agent.name,
814
+ sessionRef: agent.sessionRef,
696
815
  },
697
816
  };
698
817
  return true;
@@ -712,7 +831,8 @@ export class FileStateStore extends InMemoryStateStore {
712
831
  if (!session.delivery || session.delivery.owner !== owner || session.delivery.claimId !== claimId)
713
832
  return false;
714
833
  session.history = [...session.history, ...session.delivery.messages].slice(-CONVERSATION_HISTORY_LIMIT);
715
- if (session.agent.name === session.delivery.agent.name &&
834
+ if (session.agent &&
835
+ session.agent.name === session.delivery.agent.name &&
716
836
  session.agent.sessionRef === session.delivery.agent.sessionRef) {
717
837
  session.agent.name = agent.name;
718
838
  if (agent.sessionRef)
@@ -763,9 +883,40 @@ export class FileStateStore extends InMemoryStateStore {
763
883
  }));
764
884
  }
765
885
  async #loadFromDisk() {
886
+ return await this.#documentStore.read();
887
+ }
888
+ async #persist(document) {
889
+ await this.#documentStore.write(document);
890
+ }
891
+ async #withMutationLock(operation) {
892
+ return await this.#documentStore.runMutation(operation);
893
+ }
894
+ async #exclusive(operation) {
895
+ const result = this.#operation.then(operation, operation);
896
+ this.#operation = result.then(() => undefined, () => undefined);
897
+ return await result;
898
+ }
899
+ }
900
+ /** Node/file-backed state with the exact persistence and locking semantics used before the document seam. */
901
+ export class FileStateStore extends DocumentStateStore {
902
+ constructor(options) {
903
+ super({
904
+ batchSize: options.batchSize,
905
+ agentQuestionDedupeLimit: options.agentQuestionDedupeLimit,
906
+ isProcessAlive: options.isProcessAlive,
907
+ documentStore: new FileWatchStateDocumentStore(options.watchStatePath),
908
+ });
909
+ }
910
+ }
911
+ class FileWatchStateDocumentStore {
912
+ #watchStatePath;
913
+ constructor(watchStatePath) {
914
+ this.#watchStatePath = watchStatePath;
915
+ }
916
+ async read() {
766
917
  try {
767
918
  const parsed = JSON.parse(await readFile(this.#watchStatePath, 'utf8'));
768
- return parseDocument(parsed);
919
+ return parseWatchStateDocument(parsed);
769
920
  }
770
921
  catch (error) {
771
922
  if (!isMissingFileError(error))
@@ -773,7 +924,7 @@ export class FileStateStore extends InMemoryStateStore {
773
924
  return { version: 3, workspaces: {} };
774
925
  }
775
926
  }
776
- async #persist(document) {
927
+ async write(document) {
777
928
  const temporaryPath = `${this.#watchStatePath}.${process.pid}.${randomUUID()}.tmp`;
778
929
  try {
779
930
  const handle = await open(temporaryPath, 'wx', 0o600);
@@ -791,7 +942,7 @@ export class FileStateStore extends InMemoryStateStore {
791
942
  await rm(temporaryPath, { force: true });
792
943
  }
793
944
  }
794
- async #withMutationLock(operation) {
945
+ async runMutation(operation) {
795
946
  await mkdir(dirname(this.#watchStatePath), { recursive: true });
796
947
  const release = await lockfile.lock(this.#watchStatePath, {
797
948
  realpath: false,
@@ -812,92 +963,10 @@ export class FileStateStore extends InMemoryStateStore {
812
963
  await release();
813
964
  }
814
965
  }
815
- async #exclusive(operation) {
816
- const result = this.#operation.then(operation, operation);
817
- this.#operation = result.then(() => undefined, () => undefined);
818
- return await result;
966
+ async assertReady() {
967
+ await this.read();
819
968
  }
820
969
  }
821
- const parseDocument = (value) => {
822
- if (!isRecord(value) || !isRecord(value.workspaces)) {
823
- throw new Error('Factory GitHub watch state file is invalid');
824
- }
825
- if (value.version === 3) {
826
- const workspaces = {};
827
- for (const [workspaceId, rawWorkspace] of Object.entries(value.workspaces)) {
828
- if (!isRecord(rawWorkspace))
829
- throw new Error('Factory GitHub watch state file is invalid');
830
- const watches = rawWorkspace.githubIssueCommentWatches;
831
- const clarifications = rawWorkspace.waitingClarifications;
832
- const babysitters = rawWorkspace.babysitterSessions;
833
- const generations = rawWorkspace.babysitterGenerations;
834
- const conversations = rawWorkspace.conversationSessions;
835
- const lifecycles = rawWorkspace.dispatchLifecycles;
836
- const discoverySweep = rawWorkspace.discoverySweep;
837
- if (!isRecord(watches) ||
838
- !isRecord(clarifications) ||
839
- (babysitters !== undefined && !isRecord(babysitters)) ||
840
- (generations !== undefined && !isRecord(generations)) ||
841
- (conversations !== undefined && !isRecord(conversations)) ||
842
- (lifecycles !== undefined && !isRecord(lifecycles)) ||
843
- (discoverySweep !== undefined && !isRecord(discoverySweep))) {
844
- throw new Error('Factory GitHub watch state file is invalid');
845
- }
846
- workspaces[workspaceId] = {
847
- githubIssueCommentWatches: watches,
848
- waitingClarifications: clarifications,
849
- babysitterSessions: parseBabysitterSessions(babysitters ?? {}),
850
- babysitterGenerations: parseBabysitterGenerations(generations ?? {}),
851
- conversationSessions: parseConversationSessions(conversations ?? {}),
852
- dispatchLifecycles: (lifecycles ?? {}),
853
- discoverySweep: parseDiscoverySweepState(discoverySweep),
854
- };
855
- }
856
- return { version: 3, workspaces };
857
- }
858
- if (value.version === 2) {
859
- const workspaces = {};
860
- for (const [workspaceId, rawWorkspace] of Object.entries(value.workspaces)) {
861
- if (!isRecord(rawWorkspace))
862
- throw new Error('Factory GitHub watch state file is invalid');
863
- const watches = rawWorkspace.githubIssueCommentWatches;
864
- const clarifications = rawWorkspace.waitingClarifications;
865
- const babysitters = rawWorkspace.babysitterSessions;
866
- if (!isRecord(watches) || !isRecord(clarifications) || (babysitters !== undefined && !isRecord(babysitters))) {
867
- throw new Error('Factory GitHub watch state file is invalid');
868
- }
869
- workspaces[workspaceId] = {
870
- githubIssueCommentWatches: watches,
871
- waitingClarifications: clarifications,
872
- babysitterSessions: parseBabysitterSessions(babysitters ?? {}),
873
- babysitterGenerations: {},
874
- conversationSessions: {},
875
- dispatchLifecycles: {},
876
- discoverySweep: emptyDiscoverySweepState(),
877
- };
878
- }
879
- return { version: 3, workspaces };
880
- }
881
- if (value.version === 1) {
882
- const workspaces = {};
883
- for (const [workspaceId, watches] of Object.entries(value.workspaces)) {
884
- if (!isRecord(watches)) {
885
- throw new Error('Factory GitHub watch state file is invalid');
886
- }
887
- workspaces[workspaceId] = {
888
- githubIssueCommentWatches: watches,
889
- waitingClarifications: {},
890
- babysitterSessions: {},
891
- babysitterGenerations: {},
892
- conversationSessions: {},
893
- dispatchLifecycles: {},
894
- discoverySweep: emptyDiscoverySweepState(),
895
- };
896
- }
897
- return { version: 3, workspaces };
898
- }
899
- throw new Error('Factory GitHub watch state file is invalid');
900
- };
901
970
  const cloneWatch = (watch) => structuredClone(watch);
902
971
  const cloneClarification = (record) => structuredClone(record);
903
972
  const cloneBabysitterSession = (session) => structuredClone(session);
@@ -930,31 +999,6 @@ const processIsAlive = (pid) => {
930
999
  return error.code !== 'ESRCH';
931
1000
  }
932
1001
  };
933
- const emptyDiscoverySweepState = () => ({
934
- consecutiveOverloads: 0,
935
- backoffUntilMs: 0,
936
- lastEpoch: 0,
937
- });
938
- const parseDiscoverySweepState = (value) => {
939
- if (value === undefined)
940
- return emptyDiscoverySweepState();
941
- if (!isRecord(value))
942
- throw new Error('Factory GitHub watch state file is invalid');
943
- const checkpoint = value.checkpoint;
944
- const lease = value.lease;
945
- const lastEpoch = value.lastEpoch ?? (isRecord(lease) ? lease.epoch : 0);
946
- if (!Number.isSafeInteger(value.consecutiveOverloads) || value.consecutiveOverloads < 0 ||
947
- !Number.isSafeInteger(value.backoffUntilMs) ||
948
- !Number.isSafeInteger(lastEpoch) || lastEpoch < 0 ||
949
- (lease !== undefined && (!isRecord(lease) || typeof lease.owner !== 'string' ||
950
- !Number.isSafeInteger(lease.epoch) || !Number.isSafeInteger(lease.leaseUntilMs))) ||
951
- (checkpoint !== undefined && (!isRecord(checkpoint) || !isRecord(checkpoint.trees) ||
952
- !Number.isSafeInteger(checkpoint.updatedAtMs) ||
953
- (checkpoint.highWatermark !== undefined && typeof checkpoint.highWatermark !== 'string') ||
954
- !Object.values(checkpoint.trees).every((paths) => Array.isArray(paths) && paths.every((path) => typeof path === 'string')))))
955
- throw new Error('Factory GitHub watch state file is invalid');
956
- return structuredClone({ ...value, lastEpoch });
957
- };
958
1002
  const CONVERSATION_HISTORY_LIMIT = 50;
959
1003
  const conversationHasMessage = (session, id) => session.processedMessageIds.includes(id) ||
960
1004
  session.history.some((message) => message.id === id) ||
@@ -962,133 +1006,6 @@ const conversationHasMessage = (session, id) => session.processedMessageIds.incl
962
1006
  Boolean(session.delivery?.messages.some((message) => message.id === id));
963
1007
  const compareConversationMessages = (left, right) => left.receivedAtMs - right.receivedAtMs ||
964
1008
  (left.providerSequence ?? left.id).localeCompare(right.providerSequence ?? right.id, undefined, { numeric: true });
965
- const parseConversationSessions = (value) => {
966
- const sessions = {};
967
- for (const [conversationId, candidate] of Object.entries(value)) {
968
- if (!isRecord(candidate) || !isRecord(candidate.issue) || !isRecord(candidate.agent) || !isRecord(candidate.context)) {
969
- throw new Error('Factory GitHub watch state file is invalid');
970
- }
971
- const issue = candidate.issue;
972
- const agent = candidate.agent;
973
- const delivery = candidate.delivery;
974
- if (typeof issue.uuid !== 'string' || typeof issue.key !== 'string' || typeof issue.path !== 'string' ||
975
- typeof candidate.provider !== 'string' || typeof candidate.externalId !== 'string' ||
976
- !Object.values(candidate.context).every((value) => typeof value === 'string') ||
977
- typeof agent.name !== 'string' || typeof agent.sessionRef !== 'string' ||
978
- !validConversationMessages(candidate.history) || !validConversationMessages(candidate.pending) ||
979
- (candidate.processedMessageIds !== undefined && !validConversationMessageIds(candidate.processedMessageIds)) ||
980
- (delivery !== undefined && !validConversationDelivery(delivery) && !validLegacyConversationDelivery(delivery))) {
981
- throw new Error('Factory GitHub watch state file is invalid');
982
- }
983
- const session = structuredClone(candidate);
984
- if (delivery !== undefined && validLegacyConversationDelivery(delivery)) {
985
- session.pending = [...structuredClone(delivery.messages), ...session.pending];
986
- delete session.delivery;
987
- }
988
- session.processedMessageIds = candidate.processedMessageIds === undefined
989
- ? [...new Set([
990
- ...session.history,
991
- ...session.pending,
992
- ...(session.delivery?.messages ?? []),
993
- ].map((message) => message.id))]
994
- : [...candidate.processedMessageIds];
995
- sessions[conversationId] = session;
996
- }
997
- return sessions;
998
- };
999
- const validConversationMessages = (value) => Array.isArray(value) && value.every((message) => isRecord(message) &&
1000
- typeof message.id === 'string' && typeof message.text === 'string' &&
1001
- typeof message.receivedAtMs === 'number' &&
1002
- (message.providerSequence === undefined || typeof message.providerSequence === 'string') &&
1003
- (message.author === undefined || typeof message.author === 'string'));
1004
- const validConversationDelivery = (value) => isRecord(value) &&
1005
- typeof value.claimId === 'string' && typeof value.owner === 'string' &&
1006
- typeof value.claimedAtMs === 'number' && typeof value.attempts === 'number' &&
1007
- validConversationMessages(value.messages) && isRecord(value.agent) &&
1008
- typeof value.agent.name === 'string' && typeof value.agent.sessionRef === 'string';
1009
- const validLegacyConversationDelivery = (value) => isRecord(value) && value.claimId === undefined && value.agent === undefined &&
1010
- typeof value.owner === 'string' && typeof value.claimedAtMs === 'number' &&
1011
- typeof value.attempts === 'number' && validConversationMessages(value.messages);
1012
- const validConversationMessageIds = (value) => Array.isArray(value) && value.every((id) => typeof id === 'string');
1013
- const parseBabysitterSessions = (value) => {
1014
- const sessions = {};
1015
- for (const [key, candidate] of Object.entries(value)) {
1016
- if (!isRecord(candidate) || !isRecord(candidate.issue)) {
1017
- throw new Error('Factory GitHub watch state file is invalid');
1018
- }
1019
- const issue = candidate.issue;
1020
- if (typeof issue.uuid !== 'string' ||
1021
- typeof issue.key !== 'string' ||
1022
- typeof issue.path !== 'string' ||
1023
- typeof candidate.repo !== 'string' ||
1024
- !Number.isSafeInteger(candidate.prNumber) ||
1025
- candidate.prNumber < 1 ||
1026
- typeof candidate.agentName !== 'string' ||
1027
- (candidate.path !== undefined && typeof candidate.path !== 'string') ||
1028
- typeof candidate.critical !== 'boolean' ||
1029
- !Array.isArray(candidate.pendingKinds) ||
1030
- !candidate.pendingKinds.every((kind) => typeof kind === 'string') ||
1031
- (candidate.pendingDeliveryClaims !== undefined && (!Array.isArray(candidate.pendingDeliveryClaims) ||
1032
- !candidate.pendingDeliveryClaims.every((claim) => isRecord(claim) && typeof claim.deliveryId === 'string' && typeof claim.claimToken === 'string'))) ||
1033
- (candidate.resourceSubscription !== undefined && (!isRecord(candidate.resourceSubscription) ||
1034
- typeof candidate.resourceSubscription.subscriptionId !== 'string' ||
1035
- typeof candidate.resourceSubscription.provider !== 'string' ||
1036
- typeof candidate.resourceSubscription.resourceRef !== 'string' ||
1037
- typeof candidate.resourceSubscription.subscriberId !== 'string' ||
1038
- typeof candidate.resourceSubscription.ownerId !== 'string' ||
1039
- typeof candidate.resourceSubscription.expiresAt !== 'string' ||
1040
- (candidate.resourceSubscription.terminal !== undefined && typeof candidate.resourceSubscription.terminal !== 'boolean')))) {
1041
- throw new Error('Factory GitHub watch state file is invalid');
1042
- }
1043
- sessions[key] = {
1044
- issue: { uuid: issue.uuid, key: issue.key, path: issue.path },
1045
- repo: candidate.repo,
1046
- prNumber: candidate.prNumber,
1047
- agentName: candidate.agentName,
1048
- ...(candidate.path === undefined ? {} : { path: candidate.path }),
1049
- critical: candidate.critical,
1050
- pendingKinds: [...candidate.pendingKinds],
1051
- ...(candidate.resourceSubscription === undefined ? {} : {
1052
- resourceSubscription: {
1053
- subscriptionId: candidate.resourceSubscription.subscriptionId,
1054
- provider: candidate.resourceSubscription.provider,
1055
- resourceRef: candidate.resourceSubscription.resourceRef,
1056
- subscriberId: candidate.resourceSubscription.subscriberId,
1057
- ownerId: candidate.resourceSubscription.ownerId,
1058
- expiresAt: candidate.resourceSubscription.expiresAt,
1059
- ...(candidate.resourceSubscription.terminal === undefined ? {} : { terminal: candidate.resourceSubscription.terminal }),
1060
- },
1061
- }),
1062
- ...(candidate.pendingDeliveryClaims === undefined ? {} : {
1063
- pendingDeliveryClaims: candidate.pendingDeliveryClaims.map((claim) => ({
1064
- deliveryId: claim.deliveryId,
1065
- claimToken: claim.claimToken,
1066
- })),
1067
- }),
1068
- };
1069
- }
1070
- return sessions;
1071
- };
1072
- const parseBabysitterGenerations = (value) => {
1073
- const generations = {};
1074
- for (const [key, candidate] of Object.entries(value)) {
1075
- if (!isRecord(candidate) ||
1076
- typeof candidate.generationId !== 'string' ||
1077
- typeof candidate.agentName !== 'string' ||
1078
- !Number.isSafeInteger(candidate.claimedAtMs) ||
1079
- !Number.isSafeInteger(candidate.leaseUntilMs) ||
1080
- (candidate.phase !== 'claimed' && candidate.phase !== 'completed'))
1081
- throw new Error('Factory GitHub watch state file is invalid');
1082
- generations[key] = {
1083
- generationId: candidate.generationId,
1084
- agentName: candidate.agentName,
1085
- claimedAtMs: candidate.claimedAtMs,
1086
- leaseUntilMs: candidate.leaseUntilMs,
1087
- phase: candidate.phase,
1088
- };
1089
- }
1090
- return generations;
1091
- };
1092
1009
  const cloneLifecycle = (record) => structuredClone(record);
1093
1010
  const dispatchLifecycleLeaseMatches = (current, expected) => current === undefined
1094
1011
  ? expected === undefined
@@ -1112,6 +1029,7 @@ const dispatchLifecycleHandedOffToBabysitters = (lifecycle) => {
1112
1029
  };
1113
1030
  const emptyWorkspaceState = () => ({
1114
1031
  githubIssueCommentWatches: {},
1032
+ slackThreadWatches: {},
1115
1033
  waitingClarifications: {},
1116
1034
  babysitterSessions: {},
1117
1035
  babysitterGenerations: {},
@@ -1120,6 +1038,7 @@ const emptyWorkspaceState = () => ({
1120
1038
  discoverySweep: emptyDiscoverySweepState(),
1121
1039
  });
1122
1040
  const workspaceIsEmpty = (workspace) => Object.keys(workspace.githubIssueCommentWatches).length === 0 &&
1041
+ Object.keys(workspace.slackThreadWatches).length === 0 &&
1123
1042
  Object.keys(workspace.waitingClarifications).length === 0 &&
1124
1043
  Object.keys(workspace.babysitterSessions).length === 0 &&
1125
1044
  Object.keys(workspace.babysitterGenerations).length === 0 &&