@tomflow/proflow-platform-host 0.1.29 → 0.1.31
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/CHANGELOG.md +12 -0
- package/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/adapter.js +22 -14
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/browser-permission-context.js +6 -3
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +437 -15
- package/dist/src/monitor-rotation-coordinator.d.ts +76 -0
- package/dist/src/monitor-rotation-coordinator.js +782 -0
- package/dist/src/operation-observability.js +4 -1
- package/dist/src/product-campaign-continuation.d.ts +132 -0
- package/dist/src/product-campaign-continuation.js +279 -0
- package/dist/src/product-discussion-admission.d.ts +92 -0
- package/dist/src/product-discussion-admission.js +191 -0
- package/dist/src/product-discussion-host.d.ts +43 -0
- package/dist/src/product-discussion-host.js +167 -0
- package/dist/src/reconciliation-coordinator.d.ts +1 -0
- package/dist/src/reconciliation-coordinator.js +7 -2
- package/dist/src/role-operations.js +8 -0
- package/package.json +7 -7
- package/proflow.module.json +1 -1
|
@@ -172,6 +172,9 @@ export function createHostOperationObserver(emit) {
|
|
|
172
172
|
const errorCode = errorValue && typeof errorValue === "object"
|
|
173
173
|
? code(Reflect.get(errorValue, "code")) ?? code(Reflect.get(errorValue, "message"))
|
|
174
174
|
: undefined;
|
|
175
|
+
const sideEffectState = Reflect.get(result, "sideEffectState");
|
|
176
|
+
const hasAuthoritativeSideEffectState = typeof sideEffectState === "string" &&
|
|
177
|
+
["NOT_STARTED", "STARTED", "APPLIED", "NOT_APPLIED", "UNKNOWN"].includes(sideEffectState);
|
|
175
178
|
const entry = {
|
|
176
179
|
contract: "proflow.operation-boundary.v1",
|
|
177
180
|
eventId: `host-event:${randomUUID()}`,
|
|
@@ -194,7 +197,7 @@ export function createHostOperationObserver(emit) {
|
|
|
194
197
|
...axes,
|
|
195
198
|
decision: code(Reflect.get(result, "decision")), reason: code(Reflect.get(result, "reason")),
|
|
196
199
|
status: failed ? "FAILED" : Reflect.get(result, "decision") === "HUMAN_REQUIRED" ? "BLOCKED" : Reflect.get(result, "decision") === "DEFER" ? "DEFERRED" : "SUCCEEDED",
|
|
197
|
-
|
|
200
|
+
...(hasAuthoritativeSideEffectState ? { sideEffectState } : {}),
|
|
198
201
|
...(failed
|
|
199
202
|
? { errorCode: errorCode ?? "HOST_APPLICATION_FAILED" }
|
|
200
203
|
: {}),
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export declare const productDiscussionQueryOperationIds: Set<string>;
|
|
2
|
+
export declare const productDiscussionMutationOperationIds: Set<string>;
|
|
3
|
+
export type ProductRoleBinding = {
|
|
4
|
+
agentPackageRef: string;
|
|
5
|
+
roleRef: string;
|
|
6
|
+
};
|
|
7
|
+
export type ProductDiscussionSessionFact = {
|
|
8
|
+
discussionRef: string;
|
|
9
|
+
roleRef: string;
|
|
10
|
+
conversationRef: string;
|
|
11
|
+
conversationLocator: string;
|
|
12
|
+
campaignRef: string | null;
|
|
13
|
+
goalRevision: number | null;
|
|
14
|
+
currentIntentRef: string | null;
|
|
15
|
+
generation: number;
|
|
16
|
+
status: "ACTIVE" | "CLOSED";
|
|
17
|
+
version: number;
|
|
18
|
+
};
|
|
19
|
+
export type ProductDiscussionAdmissionVerificationInput = {
|
|
20
|
+
discussionRef: string;
|
|
21
|
+
roleRef: string;
|
|
22
|
+
conversationRef: string;
|
|
23
|
+
conversationLocator: string;
|
|
24
|
+
generation: number;
|
|
25
|
+
discussionAdmissionToken: string;
|
|
26
|
+
};
|
|
27
|
+
export type ProductContinuationProjection = {
|
|
28
|
+
campaign: {
|
|
29
|
+
campaignRef: string;
|
|
30
|
+
discussionRef: string;
|
|
31
|
+
goalRevision: number;
|
|
32
|
+
version: number;
|
|
33
|
+
status: "OPEN" | "CLOSED";
|
|
34
|
+
};
|
|
35
|
+
intent: {
|
|
36
|
+
intentRef: string;
|
|
37
|
+
version: number;
|
|
38
|
+
} | null;
|
|
39
|
+
task: {
|
|
40
|
+
taskId: string;
|
|
41
|
+
status: string;
|
|
42
|
+
version: number;
|
|
43
|
+
} | null;
|
|
44
|
+
authorization: {
|
|
45
|
+
authorizationRef: string;
|
|
46
|
+
version: number;
|
|
47
|
+
} | null;
|
|
48
|
+
requirementRevision: number | null;
|
|
49
|
+
nextAction: "NONE" | "PROVISION" | "TRANSFER_REQUIREMENT" | "CONSIDER_START" | "PRODUCT_REVIEW_REQUIRED";
|
|
50
|
+
};
|
|
51
|
+
export type ProductReviewSignal = {
|
|
52
|
+
kind: "PRODUCT_REVIEW_REQUIRED";
|
|
53
|
+
reviewRef: string;
|
|
54
|
+
campaignRef: string;
|
|
55
|
+
taskId: string;
|
|
56
|
+
terminalTaskVersion: number;
|
|
57
|
+
intentRef: string;
|
|
58
|
+
discussionRef: string;
|
|
59
|
+
discussionGeneration: number;
|
|
60
|
+
roleRef: string;
|
|
61
|
+
conversationRef: string;
|
|
62
|
+
conversationLocator: string;
|
|
63
|
+
};
|
|
64
|
+
export type ProductContinuationOutcome = {
|
|
65
|
+
kind: "NOT_CAMPAIGN";
|
|
66
|
+
taskId: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "IDLE";
|
|
69
|
+
taskId: string;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "AUTHORIZATION_REQUIRED";
|
|
72
|
+
taskId: string;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "TASK_STARTED";
|
|
75
|
+
taskId: string;
|
|
76
|
+
} | ProductReviewSignal;
|
|
77
|
+
export declare class ProductCampaignContinuationError extends Error {
|
|
78
|
+
readonly code: string;
|
|
79
|
+
readonly httpStatus: number;
|
|
80
|
+
constructor(code: string, httpStatus?: number);
|
|
81
|
+
}
|
|
82
|
+
export declare function canonicalizeProductOperation(input: {
|
|
83
|
+
operationId: string;
|
|
84
|
+
authenticatedRoleRef: string;
|
|
85
|
+
rawInput: unknown;
|
|
86
|
+
getDiscussionSession: (discussionRef: string) => ProductDiscussionSessionFact;
|
|
87
|
+
getRoleBindings: () => readonly ProductRoleBinding[];
|
|
88
|
+
verifyDiscussionAdmission?: (input: ProductDiscussionAdmissionVerificationInput) => boolean;
|
|
89
|
+
}): {
|
|
90
|
+
scope: "TASK" | "PRODUCT_QUERY" | "PRODUCT_MUTATION";
|
|
91
|
+
input: Record<string, unknown>;
|
|
92
|
+
actorRef?: string;
|
|
93
|
+
kickContinuation: boolean;
|
|
94
|
+
};
|
|
95
|
+
export declare function createProductCampaignContinuationCoordinator(options: {
|
|
96
|
+
getProjection: (taskId: string) => Promise<ProductContinuationProjection | null>;
|
|
97
|
+
listTaskIds: () => Promise<readonly string[]>;
|
|
98
|
+
ensureWorkers: (taskId: string) => Promise<void>;
|
|
99
|
+
transferRequirement: (input: {
|
|
100
|
+
intentRef: string;
|
|
101
|
+
taskId: string;
|
|
102
|
+
expectedIntentVersion: number;
|
|
103
|
+
expectedTaskVersion: number;
|
|
104
|
+
idempotencyKey: string;
|
|
105
|
+
}) => Promise<void>;
|
|
106
|
+
startTask: (input: {
|
|
107
|
+
taskId: string;
|
|
108
|
+
expectedTaskVersion: number;
|
|
109
|
+
authorizationRef: string;
|
|
110
|
+
expectedAuthorizationVersion: number;
|
|
111
|
+
expectedCampaignVersion: number;
|
|
112
|
+
idempotencyKey: string;
|
|
113
|
+
}) => Promise<void>;
|
|
114
|
+
getDiscussionSessionForCampaign: (campaignRef: string) => ProductDiscussionSessionFact | undefined;
|
|
115
|
+
updateDiscussionSessionContext: (input: {
|
|
116
|
+
discussionRef: string;
|
|
117
|
+
expectedVersion: number;
|
|
118
|
+
campaignRef: string;
|
|
119
|
+
goalRevision: number;
|
|
120
|
+
currentIntentRef: string;
|
|
121
|
+
}) => ProductDiscussionSessionFact;
|
|
122
|
+
kickTaskReconciliation: (taskId: string) => void;
|
|
123
|
+
onProductReviewRequired?: (signal: ProductReviewSignal) => void | Promise<void>;
|
|
124
|
+
retryDelayMs?: number;
|
|
125
|
+
}): Readonly<{
|
|
126
|
+
start(): void;
|
|
127
|
+
kick: (taskId: string) => void;
|
|
128
|
+
reconcile: (taskId: string) => Promise<ProductContinuationOutcome>;
|
|
129
|
+
sweep: () => Promise<ProductContinuationOutcome[]>;
|
|
130
|
+
drain(): Promise<void>;
|
|
131
|
+
stop(): void;
|
|
132
|
+
}>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
export const productDiscussionQueryOperationIds = new Set([
|
|
2
|
+
"getProductCampaign",
|
|
3
|
+
"getProductDiscussionContext",
|
|
4
|
+
"getProductDocument",
|
|
5
|
+
"getCampaignAuthorization",
|
|
6
|
+
]);
|
|
7
|
+
export const productDiscussionMutationOperationIds = new Set([
|
|
8
|
+
"submitProductTaskIntent",
|
|
9
|
+
"putProductDocument",
|
|
10
|
+
"recordProductGapReview",
|
|
11
|
+
"recordProductGoalSatisfied",
|
|
12
|
+
]);
|
|
13
|
+
export class ProductCampaignContinuationError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
httpStatus;
|
|
16
|
+
constructor(code, httpStatus = 409) {
|
|
17
|
+
super(code);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.httpStatus = httpStatus;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function record(value, label) {
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
24
|
+
throw new ProductCampaignContinuationError(`${label.toUpperCase()}_INVALID`, 400);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function requiredString(value, label) {
|
|
28
|
+
if (typeof value !== "string" || value.length === 0)
|
|
29
|
+
throw new ProductCampaignContinuationError(`${label.toUpperCase()}_REQUIRED`, 400);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function requiredGeneration(value) {
|
|
33
|
+
if (!Number.isInteger(value) || Number(value) <= 0)
|
|
34
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_GENERATION_REQUIRED", 400);
|
|
35
|
+
return Number(value);
|
|
36
|
+
}
|
|
37
|
+
export function canonicalizeProductOperation(input) {
|
|
38
|
+
const raw = record(input.rawInput, "product action input");
|
|
39
|
+
const productQuery = productDiscussionQueryOperationIds.has(input.operationId);
|
|
40
|
+
const productMutation = productDiscussionMutationOperationIds.has(input.operationId);
|
|
41
|
+
if (!productQuery && !productMutation)
|
|
42
|
+
return { scope: "TASK", input: raw, kickContinuation: false };
|
|
43
|
+
const canonical = { ...raw };
|
|
44
|
+
delete canonical.roleRef;
|
|
45
|
+
delete canonical.actorRef;
|
|
46
|
+
delete canonical.workerRef;
|
|
47
|
+
delete canonical.roleBindings;
|
|
48
|
+
const discussionRef = requiredString(canonical.discussionRef, "discussionRef");
|
|
49
|
+
const discussionGeneration = requiredGeneration(canonical.discussionGeneration);
|
|
50
|
+
const session = input.getDiscussionSession(discussionRef);
|
|
51
|
+
if (session.status !== "ACTIVE" ||
|
|
52
|
+
session.roleRef !== input.authenticatedRoleRef)
|
|
53
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_MISMATCH", 403);
|
|
54
|
+
if (session.generation !== discussionGeneration)
|
|
55
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_GENERATION_MISMATCH", 403);
|
|
56
|
+
if (!input.verifyDiscussionAdmission)
|
|
57
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_ADMISSION_UNAVAILABLE", 503);
|
|
58
|
+
const discussionAdmissionToken = requiredString(canonical.discussionAdmissionToken, "discussionAdmissionToken");
|
|
59
|
+
if (!input.verifyDiscussionAdmission({
|
|
60
|
+
discussionRef,
|
|
61
|
+
roleRef: session.roleRef,
|
|
62
|
+
conversationRef: session.conversationRef,
|
|
63
|
+
conversationLocator: session.conversationLocator,
|
|
64
|
+
generation: discussionGeneration,
|
|
65
|
+
discussionAdmissionToken,
|
|
66
|
+
}))
|
|
67
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_ADMISSION_INVALID", 403);
|
|
68
|
+
delete canonical.discussionAdmissionToken;
|
|
69
|
+
if (typeof canonical.campaignRef === "string" &&
|
|
70
|
+
session.campaignRef !== canonical.campaignRef)
|
|
71
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_CAMPAIGN_MISMATCH", 403);
|
|
72
|
+
if (typeof canonical.goalRevision === "number" &&
|
|
73
|
+
session.goalRevision !== null &&
|
|
74
|
+
session.goalRevision !== canonical.goalRevision)
|
|
75
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_GOAL_REVISION_MISMATCH", 403);
|
|
76
|
+
delete canonical.discussionGeneration;
|
|
77
|
+
if (productQuery) {
|
|
78
|
+
if (input.operationId !== "getProductDocument")
|
|
79
|
+
delete canonical.discussionRef;
|
|
80
|
+
if (input.operationId === "getProductDocument")
|
|
81
|
+
requiredString(canonical.campaignRef, "campaignRef");
|
|
82
|
+
if (input.operationId === "getCampaignAuthorization") {
|
|
83
|
+
requiredString(canonical.campaignRef, "campaignRef");
|
|
84
|
+
delete canonical.campaignRef;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
scope: "PRODUCT_QUERY",
|
|
88
|
+
input: canonical,
|
|
89
|
+
kickContinuation: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (input.operationId === "submitProductTaskIntent")
|
|
93
|
+
canonical.roleBindings = input
|
|
94
|
+
.getRoleBindings()
|
|
95
|
+
.map((binding) => ({ ...binding }));
|
|
96
|
+
return {
|
|
97
|
+
scope: "PRODUCT_MUTATION",
|
|
98
|
+
input: canonical,
|
|
99
|
+
actorRef: `product-discussion:${discussionRef}`,
|
|
100
|
+
kickContinuation: input.operationId === "submitProductTaskIntent",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function createProductCampaignContinuationCoordinator(options) {
|
|
104
|
+
const retryDelayMs = Math.max(50, options.retryDelayMs ?? 500);
|
|
105
|
+
const inFlight = new Map();
|
|
106
|
+
const retryTimers = new Map();
|
|
107
|
+
let stopped = false;
|
|
108
|
+
let scanTimer;
|
|
109
|
+
let started = false;
|
|
110
|
+
let scanInFlight;
|
|
111
|
+
const reconcileOnce = async (taskId) => {
|
|
112
|
+
for (let step = 0; step < 6; step += 1) {
|
|
113
|
+
if (stopped)
|
|
114
|
+
return { kind: "IDLE", taskId };
|
|
115
|
+
const projection = await options.getProjection(taskId);
|
|
116
|
+
if (stopped)
|
|
117
|
+
return { kind: "IDLE", taskId };
|
|
118
|
+
if (!projection)
|
|
119
|
+
return { kind: "NOT_CAMPAIGN", taskId };
|
|
120
|
+
if (projection.nextAction === "NONE")
|
|
121
|
+
return { kind: "IDLE", taskId };
|
|
122
|
+
if (!projection.task || !projection.intent)
|
|
123
|
+
throw new ProductCampaignContinuationError("PRODUCT_CONTINUATION_FACTS_INCOMPLETE");
|
|
124
|
+
if (projection.nextAction === "PROVISION") {
|
|
125
|
+
await options.ensureWorkers(taskId);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (projection.nextAction === "TRANSFER_REQUIREMENT") {
|
|
129
|
+
if (projection.requirementRevision === null)
|
|
130
|
+
throw new ProductCampaignContinuationError("PRODUCT_REQUIREMENT_REVISION_MISSING");
|
|
131
|
+
await options.transferRequirement({
|
|
132
|
+
intentRef: projection.intent.intentRef,
|
|
133
|
+
taskId,
|
|
134
|
+
expectedIntentVersion: projection.intent.version,
|
|
135
|
+
expectedTaskVersion: projection.task.version,
|
|
136
|
+
idempotencyKey: `transfer:${projection.intent.intentRef}:${taskId}:${projection.requirementRevision}`,
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (projection.nextAction === "CONSIDER_START") {
|
|
141
|
+
if (!projection.authorization)
|
|
142
|
+
return { kind: "AUTHORIZATION_REQUIRED", taskId };
|
|
143
|
+
await options.startTask({
|
|
144
|
+
taskId,
|
|
145
|
+
expectedTaskVersion: projection.task.version,
|
|
146
|
+
authorizationRef: projection.authorization.authorizationRef,
|
|
147
|
+
expectedAuthorizationVersion: projection.authorization.version,
|
|
148
|
+
expectedCampaignVersion: projection.campaign.version,
|
|
149
|
+
idempotencyKey: `auto-start:${projection.authorization.authorizationRef}:${taskId}:${projection.task.version}`,
|
|
150
|
+
});
|
|
151
|
+
options.kickTaskReconciliation(taskId);
|
|
152
|
+
return { kind: "TASK_STARTED", taskId };
|
|
153
|
+
}
|
|
154
|
+
const session = options.getDiscussionSessionForCampaign(projection.campaign.campaignRef);
|
|
155
|
+
if (!session ||
|
|
156
|
+
session.status !== "ACTIVE" ||
|
|
157
|
+
session.discussionRef !== projection.campaign.discussionRef ||
|
|
158
|
+
session.campaignRef !== projection.campaign.campaignRef ||
|
|
159
|
+
(session.goalRevision !== null &&
|
|
160
|
+
session.goalRevision !== projection.campaign.goalRevision))
|
|
161
|
+
throw new ProductCampaignContinuationError("PRODUCT_DISCUSSION_SESSION_NOT_READY");
|
|
162
|
+
const currentSession = session.currentIntentRef === projection.intent.intentRef
|
|
163
|
+
? session
|
|
164
|
+
: options.updateDiscussionSessionContext({
|
|
165
|
+
discussionRef: session.discussionRef,
|
|
166
|
+
expectedVersion: session.version,
|
|
167
|
+
campaignRef: projection.campaign.campaignRef,
|
|
168
|
+
goalRevision: projection.campaign.goalRevision,
|
|
169
|
+
currentIntentRef: projection.intent.intentRef,
|
|
170
|
+
});
|
|
171
|
+
const signal = {
|
|
172
|
+
kind: "PRODUCT_REVIEW_REQUIRED",
|
|
173
|
+
reviewRef: `product-review:${projection.campaign.campaignRef}:${taskId}:${projection.task.version}:${currentSession.discussionRef}:${currentSession.generation}`,
|
|
174
|
+
campaignRef: projection.campaign.campaignRef,
|
|
175
|
+
taskId,
|
|
176
|
+
terminalTaskVersion: projection.task.version,
|
|
177
|
+
intentRef: projection.intent.intentRef,
|
|
178
|
+
discussionRef: currentSession.discussionRef,
|
|
179
|
+
discussionGeneration: currentSession.generation,
|
|
180
|
+
roleRef: currentSession.roleRef,
|
|
181
|
+
conversationRef: currentSession.conversationRef,
|
|
182
|
+
conversationLocator: currentSession.conversationLocator,
|
|
183
|
+
};
|
|
184
|
+
await options.onProductReviewRequired?.(signal);
|
|
185
|
+
return signal;
|
|
186
|
+
}
|
|
187
|
+
throw new ProductCampaignContinuationError("PRODUCT_CONTINUATION_STEP_LIMIT");
|
|
188
|
+
};
|
|
189
|
+
const reconcile = (taskId) => {
|
|
190
|
+
const current = inFlight.get(taskId);
|
|
191
|
+
if (current)
|
|
192
|
+
return current;
|
|
193
|
+
const run = reconcileOnce(taskId).finally(() => inFlight.delete(taskId));
|
|
194
|
+
inFlight.set(taskId, run);
|
|
195
|
+
return run;
|
|
196
|
+
};
|
|
197
|
+
const scheduleRetry = (taskId) => {
|
|
198
|
+
if (stopped || retryTimers.has(taskId))
|
|
199
|
+
return;
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
retryTimers.delete(taskId);
|
|
202
|
+
if (!stopped)
|
|
203
|
+
void reconcile(taskId).catch(() => scheduleRetry(taskId));
|
|
204
|
+
}, retryDelayMs);
|
|
205
|
+
timer.unref?.();
|
|
206
|
+
retryTimers.set(taskId, timer);
|
|
207
|
+
};
|
|
208
|
+
const kick = (taskId) => {
|
|
209
|
+
if (stopped)
|
|
210
|
+
return;
|
|
211
|
+
void reconcile(taskId).catch(() => scheduleRetry(taskId));
|
|
212
|
+
};
|
|
213
|
+
const sweep = async () => {
|
|
214
|
+
if (stopped)
|
|
215
|
+
return [];
|
|
216
|
+
const taskIds = await options.listTaskIds();
|
|
217
|
+
const outcomes = [];
|
|
218
|
+
const failures = [];
|
|
219
|
+
for (let index = 0; !stopped && index < taskIds.length; index += 4) {
|
|
220
|
+
const batch = await Promise.allSettled(taskIds.slice(index, index + 4).map((taskId) => reconcile(taskId).catch((error) => {
|
|
221
|
+
scheduleRetry(taskId);
|
|
222
|
+
throw error;
|
|
223
|
+
})));
|
|
224
|
+
for (const result of batch) {
|
|
225
|
+
if (result.status === "fulfilled")
|
|
226
|
+
outcomes.push(result.value);
|
|
227
|
+
else
|
|
228
|
+
failures.push(result.reason);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (failures.length > 0)
|
|
232
|
+
throw failures[0];
|
|
233
|
+
return outcomes;
|
|
234
|
+
};
|
|
235
|
+
const scan = async () => {
|
|
236
|
+
let delay = 30_000;
|
|
237
|
+
try {
|
|
238
|
+
await sweep();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
delay = retryDelayMs;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
if (!stopped) {
|
|
245
|
+
scanTimer = setTimeout(() => {
|
|
246
|
+
scanInFlight = scan();
|
|
247
|
+
}, delay);
|
|
248
|
+
scanTimer.unref?.();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
return Object.freeze({
|
|
253
|
+
start() {
|
|
254
|
+
if (stopped)
|
|
255
|
+
throw new ProductCampaignContinuationError("PRODUCT_CONTINUATION_STOPPED");
|
|
256
|
+
if (started)
|
|
257
|
+
return;
|
|
258
|
+
started = true;
|
|
259
|
+
scanInFlight = scan();
|
|
260
|
+
},
|
|
261
|
+
kick,
|
|
262
|
+
reconcile,
|
|
263
|
+
sweep,
|
|
264
|
+
async drain() {
|
|
265
|
+
await Promise.allSettled([
|
|
266
|
+
...(scanInFlight ? [scanInFlight] : []),
|
|
267
|
+
...inFlight.values(),
|
|
268
|
+
]);
|
|
269
|
+
},
|
|
270
|
+
stop() {
|
|
271
|
+
stopped = true;
|
|
272
|
+
if (scanTimer)
|
|
273
|
+
clearTimeout(scanTimer);
|
|
274
|
+
for (const timer of retryTimers.values())
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
retryTimers.clear();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type ProductDiscussionConversationIdentity = Readonly<{
|
|
2
|
+
roleRef: string;
|
|
3
|
+
conversationRef: string;
|
|
4
|
+
conversationLocator: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type ProductDiscussionAdmission = Readonly<{
|
|
7
|
+
discussionRef: string;
|
|
8
|
+
roleRef: string;
|
|
9
|
+
conversationRef: string;
|
|
10
|
+
conversationLocator: string;
|
|
11
|
+
generation: number;
|
|
12
|
+
contentInstanceId: string;
|
|
13
|
+
bootstrapRef: string;
|
|
14
|
+
discussionAdmissionToken: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type ProductDiscussionAdmissionVerification = Readonly<{
|
|
17
|
+
discussionRef: string;
|
|
18
|
+
roleRef: string;
|
|
19
|
+
conversationRef: string;
|
|
20
|
+
conversationLocator: string;
|
|
21
|
+
generation: number;
|
|
22
|
+
discussionAdmissionToken: string;
|
|
23
|
+
}>;
|
|
24
|
+
export type ProductDiscussionSessionOwnerFact = Readonly<{
|
|
25
|
+
discussionRef: string;
|
|
26
|
+
roleRef: string;
|
|
27
|
+
conversationRef: string;
|
|
28
|
+
conversationLocator: string;
|
|
29
|
+
generation: number;
|
|
30
|
+
status: "ACTIVE" | "CLOSED";
|
|
31
|
+
}>;
|
|
32
|
+
export type ProductDiscussionTaskRoleBindingFact = Readonly<{
|
|
33
|
+
roleRef: string;
|
|
34
|
+
workerRef: string | null;
|
|
35
|
+
conversationLocator: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
type RandomBytes = (size: number) => Uint8Array;
|
|
38
|
+
export declare function parseProductDiscussionConversationLocator(value: string): ProductDiscussionConversationIdentity | null;
|
|
39
|
+
export declare function productDiscussionRef(identity: ProductDiscussionConversationIdentity): string;
|
|
40
|
+
export declare function createProductDiscussionAdmissionAuthority(options?: {
|
|
41
|
+
randomBytesImpl?: RandomBytes;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}): Readonly<{
|
|
44
|
+
ensure: (input: {
|
|
45
|
+
discussionRef: string;
|
|
46
|
+
roleRef: string;
|
|
47
|
+
conversationRef: string;
|
|
48
|
+
conversationLocator: string;
|
|
49
|
+
generation: number;
|
|
50
|
+
contentInstanceId: string;
|
|
51
|
+
}) => ProductDiscussionAdmission;
|
|
52
|
+
verify: (input: ProductDiscussionAdmissionVerification) => boolean;
|
|
53
|
+
get(discussionRef: string): ProductDiscussionAdmission | undefined;
|
|
54
|
+
revoke(discussionRef: string): boolean;
|
|
55
|
+
clear(): void;
|
|
56
|
+
}>;
|
|
57
|
+
export declare function assertProductDiscussionConversationPreTask(options: {
|
|
58
|
+
conversationLocator: string;
|
|
59
|
+
listTaskIds(): readonly string[] | Promise<readonly string[]>;
|
|
60
|
+
getTaskRoleBindings(taskId: string): readonly ProductDiscussionTaskRoleBindingFact[] | Promise<readonly ProductDiscussionTaskRoleBindingFact[]>;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
export declare function createProductDiscussionAdmissionCoordinator(options: {
|
|
63
|
+
productRoleRef: string;
|
|
64
|
+
authority: ReturnType<typeof createProductDiscussionAdmissionAuthority>;
|
|
65
|
+
isTaskWorkerProvisioning(roleRef: string): boolean;
|
|
66
|
+
listTaskIds(): readonly string[] | Promise<readonly string[]>;
|
|
67
|
+
getTaskRoleBindings(taskId: string): readonly ProductDiscussionTaskRoleBindingFact[] | Promise<readonly ProductDiscussionTaskRoleBindingFact[]>;
|
|
68
|
+
registerDiscussionSession(input: {
|
|
69
|
+
discussionRef: string;
|
|
70
|
+
roleRef: string;
|
|
71
|
+
conversationRef: string;
|
|
72
|
+
conversationLocator: string;
|
|
73
|
+
}): ProductDiscussionSessionOwnerFact | Promise<ProductDiscussionSessionOwnerFact>;
|
|
74
|
+
deliverBootstrap(admission: ProductDiscussionAdmission): void | Promise<void>;
|
|
75
|
+
}): Readonly<{
|
|
76
|
+
observe: (input: {
|
|
77
|
+
conversationLocator: string;
|
|
78
|
+
contentInstanceId: string;
|
|
79
|
+
}) => Promise<Readonly<{
|
|
80
|
+
admitted: false;
|
|
81
|
+
reason: "NOT_CUSTOM_GPT_CONVERSATION";
|
|
82
|
+
}> | Readonly<{
|
|
83
|
+
admitted: false;
|
|
84
|
+
reason: "NOT_PRODUCT_ROLE";
|
|
85
|
+
}> | Readonly<{
|
|
86
|
+
admitted: true;
|
|
87
|
+
discussionRef: string;
|
|
88
|
+
discussionGeneration: number;
|
|
89
|
+
bootstrapRef: string;
|
|
90
|
+
}>>;
|
|
91
|
+
}>;
|
|
92
|
+
export {};
|