@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.
- package/README.md +117 -15
- package/dist/cli/exit-codes.d.ts +81 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +122 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/fleet.d.ts +26 -2
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +183 -40
- package/dist/cli/fleet.js.map +1 -1
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/schema.d.ts +156 -52
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +17 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/dispatch/templates.d.ts +7 -0
- package/dist/dispatch/templates.d.ts.map +1 -1
- package/dist/dispatch/templates.js +64 -12
- package/dist/dispatch/templates.js.map +1 -1
- package/dist/dispatch/work-unit-identity.d.ts +19 -0
- package/dist/dispatch/work-unit-identity.d.ts.map +1 -0
- package/dist/dispatch/work-unit-identity.js +40 -0
- package/dist/dispatch/work-unit-identity.js.map +1 -0
- package/dist/environments/stack-descriptor.d.ts +3 -3
- package/dist/fleet/control-plane-circuit.d.ts +57 -0
- package/dist/fleet/control-plane-circuit.d.ts.map +1 -0
- package/dist/fleet/control-plane-circuit.js +231 -0
- package/dist/fleet/control-plane-circuit.js.map +1 -0
- package/dist/fleet/internal-fleet-client.d.ts +2 -1
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +11 -7
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +3 -0
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +97 -18
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/github/writeback-paths.d.ts +7 -0
- package/dist/github/writeback-paths.d.ts.map +1 -0
- package/dist/github/writeback-paths.js +28 -0
- package/dist/github/writeback-paths.js.map +1 -0
- package/dist/hosted/orchestrator.d.ts.map +1 -1
- package/dist/hosted/orchestrator.js +2 -0
- package/dist/hosted/orchestrator.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/intake/index.d.ts +2 -1
- package/dist/intake/index.d.ts.map +1 -1
- package/dist/intake/index.js +2 -1
- package/dist/intake/index.js.map +1 -1
- package/dist/intake/notion-manifest.d.ts +48 -0
- package/dist/intake/notion-manifest.d.ts.map +1 -0
- package/dist/intake/notion-manifest.js +258 -0
- package/dist/intake/notion-manifest.js.map +1 -0
- package/dist/intake/notion-relay-claim.d.ts +5 -4
- package/dist/intake/notion-relay-claim.d.ts.map +1 -1
- package/dist/intake/notion-relay-claim.js +48 -7
- package/dist/intake/notion-relay-claim.js.map +1 -1
- package/dist/intake/notion.d.ts +6 -3
- package/dist/intake/notion.d.ts.map +1 -1
- package/dist/intake/notion.js +63 -13
- package/dist/intake/notion.js.map +1 -1
- package/dist/issue-key-match.d.ts +6 -0
- package/dist/issue-key-match.d.ts.map +1 -1
- package/dist/issue-key-match.js +6 -1
- package/dist/issue-key-match.js.map +1 -1
- package/dist/mount/local-mount-preflight.d.ts.map +1 -1
- package/dist/mount/local-mount-preflight.js +6 -2
- package/dist/mount/local-mount-preflight.js.map +1 -1
- package/dist/mount/mount-auth-error.d.ts +16 -3
- package/dist/mount/mount-auth-error.d.ts.map +1 -1
- package/dist/mount/mount-auth-error.js +34 -7
- package/dist/mount/mount-auth-error.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +36 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +136 -6
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/mount/relayfile-github-connection-write.d.ts +24 -2
- package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
- package/dist/mount/relayfile-github-connection-write.js +90 -1
- package/dist/mount/relayfile-github-connection-write.js.map +1 -1
- package/dist/node/factory-node.d.ts.map +1 -1
- package/dist/node/factory-node.js +18 -4
- package/dist/node/factory-node.js.map +1 -1
- package/dist/observability/cloud-reporter.d.ts +1 -0
- package/dist/observability/cloud-reporter.d.ts.map +1 -1
- package/dist/observability/cloud-reporter.js +182 -50
- package/dist/observability/cloud-reporter.js.map +1 -1
- package/dist/observability/events.d.ts +52 -52
- package/dist/orchestrator/batch-tracker.d.ts +25 -0
- package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
- package/dist/orchestrator/batch-tracker.js +48 -2
- package/dist/orchestrator/batch-tracker.js.map +1 -1
- package/dist/orchestrator/factory.d.ts +36 -2
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +1208 -82
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +1 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/ports/fleet.d.ts +6 -0
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/mount.d.ts +39 -0
- package/dist/ports/mount.d.ts.map +1 -1
- package/dist/ports/state.d.ts +75 -4
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/state/document-store.d.ts +33 -0
- package/dist/state/document-store.d.ts.map +1 -0
- package/dist/state/document-store.js +7 -0
- package/dist/state/document-store.js.map +1 -0
- package/dist/state/file-state-store.d.ts +32 -7
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +169 -250
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +13 -2
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +87 -4
- package/dist/state/in-memory-state-store.js.map +1 -1
- package/dist/state/watch-state-document.d.ts +5 -0
- package/dist/state/watch-state-document.d.ts.map +1 -0
- package/dist/state/watch-state-document.js +486 -0
- package/dist/state/watch-state-document.js.map +1 -0
- package/dist/testing/deadline.d.ts +11 -0
- package/dist/testing/deadline.d.ts.map +1 -0
- package/dist/testing/deadline.js +23 -0
- package/dist/testing/deadline.js.map +1 -0
- package/dist/testing/fakes.d.ts +2 -0
- package/dist/testing/fakes.d.ts.map +1 -1
- package/dist/testing/fakes.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/trajectory.d.ts +25 -0
- package/dist/trajectory.d.ts.map +1 -0
- package/dist/trajectory.js +51 -0
- package/dist/trajectory.js.map +1 -0
- package/dist/triage/heuristic.d.ts +5 -0
- package/dist/triage/heuristic.d.ts.map +1 -1
- package/dist/triage/heuristic.js +42 -3
- package/dist/triage/heuristic.js.map +1 -1
- package/dist/triage/index.d.ts +1 -1
- package/dist/triage/index.d.ts.map +1 -1
- package/dist/triage/index.js +1 -1
- package/dist/triage/index.js.map +1 -1
- package/dist/triage/schema.d.ts +73 -55
- package/dist/triage/schema.d.ts.map +1 -1
- package/dist/triage/schema.js +2 -1
- package/dist/triage/schema.js.map +1 -1
- package/dist/types.d.ts +19 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/writeback/github.d.ts +22 -7
- package/dist/writeback/github.d.ts.map +1 -1
- package/dist/writeback/github.js +86 -16
- package/dist/writeback/github.js.map +1 -1
- package/dist/writeback/index.d.ts +1 -1
- package/dist/writeback/index.d.ts.map +1 -1
- package/dist/writeback/index.js +1 -1
- package/dist/writeback/index.js.map +1 -1
- 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
|
|
18
|
-
* ownership, and thread-owned conversation turns atomically so
|
|
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
|
|
24
|
-
|
|
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
|
|
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
|
|
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:
|
|
695
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
816
|
-
|
|
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 &&
|