@tomflow/proflow-platform-host 0.1.30 → 0.1.32
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/index.d.ts +5 -0
- package/dist/src/index.js +503 -15
- package/dist/src/monitor-drive-relay.d.ts +13 -0
- package/dist/src/monitor-drive-relay.js +107 -0
- package/dist/src/monitor-rotation-coordinator.d.ts +13 -0
- package/dist/src/monitor-rotation-coordinator.js +24 -0
- 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 +11 -11
- package/proflow.module.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const productRoleRef = /^g-[A-Za-z0-9_-]+$/;
|
|
3
|
+
const bootstrapPrefix = "product-discussion-bootstrap:";
|
|
4
|
+
const tokenBytes = 32;
|
|
5
|
+
const bootstrapRefBytes = 16;
|
|
6
|
+
function requiredText(value, code) {
|
|
7
|
+
if (value.trim().length === 0)
|
|
8
|
+
throw new Error(code);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function positiveInteger(value, code) {
|
|
12
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
13
|
+
throw new Error(code);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function randomBuffer(random, size) {
|
|
17
|
+
const bytes = Buffer.from(random(size));
|
|
18
|
+
if (bytes.byteLength !== size)
|
|
19
|
+
throw new Error("PRODUCT_DISCUSSION_ADMISSION_RANDOM_SOURCE_INVALID");
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
function secretEqual(left, right) {
|
|
23
|
+
const a = Buffer.from(left);
|
|
24
|
+
const b = Buffer.from(right);
|
|
25
|
+
return a.byteLength === b.byteLength && timingSafeEqual(a, b);
|
|
26
|
+
}
|
|
27
|
+
export function parseProductDiscussionConversationLocator(value) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = new URL(value);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (parsed.protocol !== "https:" ||
|
|
36
|
+
parsed.hostname !== "chatgpt.com" ||
|
|
37
|
+
parsed.username !== "" ||
|
|
38
|
+
parsed.password !== "" ||
|
|
39
|
+
parsed.search !== "" ||
|
|
40
|
+
parsed.hash !== "")
|
|
41
|
+
return null;
|
|
42
|
+
const match = /^\/g\/(g-[A-Za-z0-9_-]+)\/c\/([^/]+)$/.exec(parsed.pathname);
|
|
43
|
+
if (!match?.[1] || !match[2] || !productRoleRef.test(match[1]))
|
|
44
|
+
return null;
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
roleRef: match[1],
|
|
47
|
+
conversationRef: match[2],
|
|
48
|
+
conversationLocator: parsed.href,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function productDiscussionRef(identity) {
|
|
52
|
+
return `product-discussion:${createHash("sha256")
|
|
53
|
+
.update(JSON.stringify([
|
|
54
|
+
identity.roleRef,
|
|
55
|
+
identity.conversationRef,
|
|
56
|
+
identity.conversationLocator,
|
|
57
|
+
]))
|
|
58
|
+
.digest("hex")}`;
|
|
59
|
+
}
|
|
60
|
+
export function createProductDiscussionAdmissionAuthority(options = {}) {
|
|
61
|
+
const random = options.randomBytesImpl ?? randomBytes;
|
|
62
|
+
const admissions = new Map();
|
|
63
|
+
const expiresAt = new Map();
|
|
64
|
+
const now = options.now ?? Date.now;
|
|
65
|
+
const ttlMs = 30 * 60_000;
|
|
66
|
+
const mint = (input) => Object.freeze({
|
|
67
|
+
discussionRef: input.discussionRef,
|
|
68
|
+
roleRef: input.identity.roleRef,
|
|
69
|
+
conversationRef: input.identity.conversationRef,
|
|
70
|
+
conversationLocator: input.identity.conversationLocator,
|
|
71
|
+
generation: input.generation,
|
|
72
|
+
contentInstanceId: input.contentInstanceId,
|
|
73
|
+
bootstrapRef: `${bootstrapPrefix}${randomBuffer(random, bootstrapRefBytes).toString("hex")}`,
|
|
74
|
+
discussionAdmissionToken: randomBuffer(random, tokenBytes).toString("base64url"),
|
|
75
|
+
});
|
|
76
|
+
const ensure = (input) => {
|
|
77
|
+
const discussionRef = requiredText(input.discussionRef, "PRODUCT_DISCUSSION_REF_REQUIRED");
|
|
78
|
+
const contentInstanceId = requiredText(input.contentInstanceId, "PRODUCT_DISCUSSION_CONTENT_INSTANCE_REQUIRED");
|
|
79
|
+
const generation = positiveInteger(input.generation, "PRODUCT_DISCUSSION_GENERATION_REQUIRED");
|
|
80
|
+
const identity = parseProductDiscussionConversationLocator(input.conversationLocator);
|
|
81
|
+
if (!identity ||
|
|
82
|
+
identity.roleRef !== input.roleRef ||
|
|
83
|
+
identity.conversationRef !== input.conversationRef)
|
|
84
|
+
throw new Error("PRODUCT_DISCUSSION_ADMISSION_IDENTITY_MISMATCH");
|
|
85
|
+
const current = admissions.get(discussionRef);
|
|
86
|
+
if (current) {
|
|
87
|
+
if (current.roleRef !== identity.roleRef ||
|
|
88
|
+
current.conversationRef !== identity.conversationRef ||
|
|
89
|
+
current.conversationLocator !== identity.conversationLocator)
|
|
90
|
+
throw new Error("PRODUCT_DISCUSSION_ADMISSION_IDENTITY_MISMATCH");
|
|
91
|
+
if (generation < current.generation)
|
|
92
|
+
throw new Error("PRODUCT_DISCUSSION_ADMISSION_GENERATION_STALE");
|
|
93
|
+
if (generation === current.generation &&
|
|
94
|
+
current.contentInstanceId === contentInstanceId &&
|
|
95
|
+
(expiresAt.get(discussionRef) ?? 0) > now())
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
98
|
+
const admission = mint({
|
|
99
|
+
discussionRef,
|
|
100
|
+
identity,
|
|
101
|
+
generation,
|
|
102
|
+
contentInstanceId,
|
|
103
|
+
});
|
|
104
|
+
admissions.set(discussionRef, admission);
|
|
105
|
+
expiresAt.set(discussionRef, now() + ttlMs);
|
|
106
|
+
return admission;
|
|
107
|
+
};
|
|
108
|
+
const verify = (input) => {
|
|
109
|
+
const current = admissions.get(input.discussionRef);
|
|
110
|
+
return Boolean(current &&
|
|
111
|
+
(expiresAt.get(input.discussionRef) ?? 0) > now() &&
|
|
112
|
+
current.roleRef === input.roleRef &&
|
|
113
|
+
current.conversationRef === input.conversationRef &&
|
|
114
|
+
current.conversationLocator === input.conversationLocator &&
|
|
115
|
+
current.generation === input.generation &&
|
|
116
|
+
secretEqual(current.discussionAdmissionToken, input.discussionAdmissionToken));
|
|
117
|
+
};
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
ensure,
|
|
120
|
+
verify,
|
|
121
|
+
get(discussionRef) {
|
|
122
|
+
return admissions.get(discussionRef);
|
|
123
|
+
},
|
|
124
|
+
revoke(discussionRef) {
|
|
125
|
+
expiresAt.delete(discussionRef);
|
|
126
|
+
return admissions.delete(discussionRef);
|
|
127
|
+
},
|
|
128
|
+
clear() {
|
|
129
|
+
admissions.clear();
|
|
130
|
+
expiresAt.clear();
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export async function assertProductDiscussionConversationPreTask(options) {
|
|
135
|
+
const taskIds = await options.listTaskIds();
|
|
136
|
+
for (const taskId of taskIds) {
|
|
137
|
+
const bindings = await options.getTaskRoleBindings(taskId);
|
|
138
|
+
if (bindings.some((binding) => binding.conversationLocator === options.conversationLocator))
|
|
139
|
+
throw new Error("PRODUCT_DISCUSSION_TASK_WORKER_FORBIDDEN");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function createProductDiscussionAdmissionCoordinator(options) {
|
|
143
|
+
if (!productRoleRef.test(options.productRoleRef))
|
|
144
|
+
throw new Error("PRODUCT_DISCUSSION_ROLE_INVALID");
|
|
145
|
+
const observe = async (input) => {
|
|
146
|
+
const contentInstanceId = requiredText(input.contentInstanceId, "PRODUCT_DISCUSSION_CONTENT_INSTANCE_REQUIRED");
|
|
147
|
+
const identity = parseProductDiscussionConversationLocator(input.conversationLocator);
|
|
148
|
+
if (!identity)
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
admitted: false,
|
|
151
|
+
reason: "NOT_CUSTOM_GPT_CONVERSATION",
|
|
152
|
+
});
|
|
153
|
+
if (identity.roleRef !== options.productRoleRef)
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
admitted: false,
|
|
156
|
+
reason: "NOT_PRODUCT_ROLE",
|
|
157
|
+
});
|
|
158
|
+
if (options.isTaskWorkerProvisioning(identity.roleRef))
|
|
159
|
+
throw new Error("PRODUCT_DISCUSSION_TASK_WORKER_PROVISIONING");
|
|
160
|
+
await assertProductDiscussionConversationPreTask({
|
|
161
|
+
conversationLocator: identity.conversationLocator,
|
|
162
|
+
listTaskIds: options.listTaskIds,
|
|
163
|
+
getTaskRoleBindings: options.getTaskRoleBindings,
|
|
164
|
+
});
|
|
165
|
+
const discussionRef = productDiscussionRef(identity);
|
|
166
|
+
const session = await options.registerDiscussionSession({
|
|
167
|
+
discussionRef,
|
|
168
|
+
...identity,
|
|
169
|
+
});
|
|
170
|
+
if (session.status !== "ACTIVE" ||
|
|
171
|
+
session.discussionRef !== discussionRef ||
|
|
172
|
+
session.roleRef !== identity.roleRef ||
|
|
173
|
+
session.conversationRef !== identity.conversationRef ||
|
|
174
|
+
session.conversationLocator !== identity.conversationLocator)
|
|
175
|
+
throw new Error("PRODUCT_DISCUSSION_SESSION_IDENTITY_MISMATCH");
|
|
176
|
+
const admission = options.authority.ensure({
|
|
177
|
+
discussionRef,
|
|
178
|
+
...identity,
|
|
179
|
+
generation: session.generation,
|
|
180
|
+
contentInstanceId,
|
|
181
|
+
});
|
|
182
|
+
await options.deliverBootstrap(admission);
|
|
183
|
+
return Object.freeze({
|
|
184
|
+
admitted: true,
|
|
185
|
+
discussionRef,
|
|
186
|
+
discussionGeneration: session.generation,
|
|
187
|
+
bootstrapRef: admission.bootstrapRef,
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
return Object.freeze({ observe });
|
|
191
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExecuteCapabilityRequest } from "@tomflow/proflow-execution-contracts";
|
|
2
|
+
import { type ProductDiscussionAdmissionVerification, type ProductDiscussionSessionOwnerFact, type ProductDiscussionTaskRoleBindingFact } from "./product-discussion-admission.ts";
|
|
3
|
+
export type ProductDiscussionHostTaskFact = Readonly<{
|
|
4
|
+
taskId: string;
|
|
5
|
+
status: string;
|
|
6
|
+
roleBindings: readonly ProductDiscussionTaskRoleBindingFact[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare function createProductDiscussionHost(options: {
|
|
9
|
+
productRoleRef: string;
|
|
10
|
+
listTaskIds(): readonly string[] | Promise<readonly string[]>;
|
|
11
|
+
getTaskFact(taskId: string): ProductDiscussionHostTaskFact | Promise<ProductDiscussionHostTaskFact>;
|
|
12
|
+
registerDiscussionSession(input: {
|
|
13
|
+
discussionRef: string;
|
|
14
|
+
roleRef: string;
|
|
15
|
+
conversationRef: string;
|
|
16
|
+
conversationLocator: string;
|
|
17
|
+
}): ProductDiscussionSessionOwnerFact | Promise<ProductDiscussionSessionOwnerFact>;
|
|
18
|
+
getDiscussionSession(discussionRef: string): ProductDiscussionSessionOwnerFact | Promise<ProductDiscussionSessionOwnerFact>;
|
|
19
|
+
executeCapability(request: ExecuteCapabilityRequest): Promise<unknown>;
|
|
20
|
+
}): Readonly<{
|
|
21
|
+
recoverReservations: () => Promise<void>;
|
|
22
|
+
reserveTaskWorker: (taskId: string, roleRef: string) => void;
|
|
23
|
+
releaseTaskWorker: (taskId: string, roleRef: string) => void;
|
|
24
|
+
isTaskWorkerProvisioning: (roleRef: string) => boolean;
|
|
25
|
+
observe: (input: {
|
|
26
|
+
conversationLocator: string;
|
|
27
|
+
contentInstanceId: string;
|
|
28
|
+
}) => Promise<Readonly<{
|
|
29
|
+
admitted: false;
|
|
30
|
+
reason: "NOT_CUSTOM_GPT_CONVERSATION";
|
|
31
|
+
}> | Readonly<{
|
|
32
|
+
admitted: false;
|
|
33
|
+
reason: "NOT_PRODUCT_ROLE";
|
|
34
|
+
}> | Readonly<{
|
|
35
|
+
admitted: true;
|
|
36
|
+
discussionRef: string;
|
|
37
|
+
discussionGeneration: number;
|
|
38
|
+
bootstrapRef: string;
|
|
39
|
+
}>>;
|
|
40
|
+
authorizeBootstrapExecution: (request: ExecuteCapabilityRequest) => Promise<boolean>;
|
|
41
|
+
verifyAdmission(input: ProductDiscussionAdmissionVerification): boolean;
|
|
42
|
+
clear(): void;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { assertProductDiscussionConversationPreTask, createProductDiscussionAdmissionAuthority, createProductDiscussionAdmissionCoordinator, parseProductDiscussionConversationLocator, productDiscussionRef, } from "./product-discussion-admission.js";
|
|
2
|
+
const reservableTaskStatuses = new Set(["PENDING", "READY", "ACTIVE", "WAITING"]);
|
|
3
|
+
function object(value, code) {
|
|
4
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
5
|
+
throw new Error(code);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function createProductDiscussionHost(options) {
|
|
9
|
+
const authority = createProductDiscussionAdmissionAuthority();
|
|
10
|
+
const provisioning = new Map();
|
|
11
|
+
let recovered = false;
|
|
12
|
+
const reserveTaskWorker = (taskId, roleRef) => {
|
|
13
|
+
if (roleRef !== options.productRoleRef)
|
|
14
|
+
return;
|
|
15
|
+
const current = provisioning.get(roleRef) ?? new Set();
|
|
16
|
+
current.add(taskId);
|
|
17
|
+
provisioning.set(roleRef, current);
|
|
18
|
+
};
|
|
19
|
+
const releaseTaskWorker = (taskId, roleRef) => {
|
|
20
|
+
if (roleRef !== options.productRoleRef)
|
|
21
|
+
return;
|
|
22
|
+
const current = provisioning.get(roleRef);
|
|
23
|
+
if (!current)
|
|
24
|
+
return;
|
|
25
|
+
current.delete(taskId);
|
|
26
|
+
if (current.size === 0)
|
|
27
|
+
provisioning.delete(roleRef);
|
|
28
|
+
};
|
|
29
|
+
const isTaskWorkerProvisioning = (roleRef) => roleRef === options.productRoleRef &&
|
|
30
|
+
(!recovered || (provisioning.get(roleRef)?.size ?? 0) > 0);
|
|
31
|
+
const coordinator = createProductDiscussionAdmissionCoordinator({
|
|
32
|
+
productRoleRef: options.productRoleRef,
|
|
33
|
+
authority,
|
|
34
|
+
isTaskWorkerProvisioning,
|
|
35
|
+
listTaskIds: options.listTaskIds,
|
|
36
|
+
async getTaskRoleBindings(taskId) {
|
|
37
|
+
return (await options.getTaskFact(taskId)).roleBindings;
|
|
38
|
+
},
|
|
39
|
+
registerDiscussionSession: options.registerDiscussionSession,
|
|
40
|
+
async deliverBootstrap(admission) {
|
|
41
|
+
const request = {
|
|
42
|
+
contract: "execution",
|
|
43
|
+
contractVersion: "1.0.0",
|
|
44
|
+
idempotencyKey: admission.bootstrapRef,
|
|
45
|
+
callerRef: "platform-host:product-discussion-admission",
|
|
46
|
+
correlationId: admission.bootstrapRef,
|
|
47
|
+
roleRef: admission.roleRef,
|
|
48
|
+
capability: "product.discussion.bootstrap.deliver",
|
|
49
|
+
input: {
|
|
50
|
+
bootstrapRef: admission.bootstrapRef,
|
|
51
|
+
discussionRef: admission.discussionRef,
|
|
52
|
+
discussionGeneration: admission.generation,
|
|
53
|
+
discussionAdmissionToken: admission.discussionAdmissionToken,
|
|
54
|
+
roleRef: admission.roleRef,
|
|
55
|
+
conversationRef: admission.conversationRef,
|
|
56
|
+
conversationLocator: admission.conversationLocator,
|
|
57
|
+
contentInstanceId: admission.contentInstanceId,
|
|
58
|
+
contentFingerprint: admission.bootstrapRef,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const execution = object(await options.executeCapability(request), "PRODUCT_DISCUSSION_BOOTSTRAP_EXECUTION_INVALID");
|
|
62
|
+
const result = execution.result === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: object(execution.result, "PRODUCT_DISCUSSION_BOOTSTRAP_RESULT_INVALID");
|
|
65
|
+
const data = result?.data === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: object(result.data, "PRODUCT_DISCUSSION_BOOTSTRAP_RESULT_DATA_INVALID");
|
|
68
|
+
if (execution.status !== "SUCCEEDED" ||
|
|
69
|
+
execution.sideEffectState !== "APPLIED" ||
|
|
70
|
+
result?.capability !== "product.discussion.bootstrap.deliver" ||
|
|
71
|
+
data?.bootstrapRef !== admission.bootstrapRef ||
|
|
72
|
+
data?.delivered !== true ||
|
|
73
|
+
typeof data.evidenceRef !== "string")
|
|
74
|
+
throw new Error(`PRODUCT_DISCUSSION_BOOTSTRAP_NOT_CONFIRMED:${String(execution.status)}:${String(execution.sideEffectState)}`);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const recoverReservations = async () => {
|
|
78
|
+
if (recovered)
|
|
79
|
+
return;
|
|
80
|
+
const taskIds = await options.listTaskIds();
|
|
81
|
+
for (const taskId of taskIds) {
|
|
82
|
+
const task = await options.getTaskFact(taskId);
|
|
83
|
+
if (!reservableTaskStatuses.has(task.status))
|
|
84
|
+
continue;
|
|
85
|
+
for (const binding of task.roleBindings)
|
|
86
|
+
if (binding.roleRef === options.productRoleRef &&
|
|
87
|
+
(!binding.workerRef || !binding.conversationLocator))
|
|
88
|
+
reserveTaskWorker(task.taskId, binding.roleRef);
|
|
89
|
+
}
|
|
90
|
+
recovered = true;
|
|
91
|
+
};
|
|
92
|
+
const authorizeBootstrapExecution = async (request) => {
|
|
93
|
+
if (request.capability !== "product.discussion.bootstrap.deliver")
|
|
94
|
+
return false;
|
|
95
|
+
try {
|
|
96
|
+
if (!recovered)
|
|
97
|
+
return false;
|
|
98
|
+
if (request.callerRef !== "platform-host:product-discussion-admission")
|
|
99
|
+
return false;
|
|
100
|
+
if (request.taskId !== undefined ||
|
|
101
|
+
request.nodeId !== undefined ||
|
|
102
|
+
request.runNo !== undefined ||
|
|
103
|
+
request.workerRef !== undefined ||
|
|
104
|
+
request.projectRoot !== undefined)
|
|
105
|
+
return false;
|
|
106
|
+
const input = request.input;
|
|
107
|
+
if (request.roleRef !== options.productRoleRef ||
|
|
108
|
+
input.roleRef !== options.productRoleRef ||
|
|
109
|
+
isTaskWorkerProvisioning(input.roleRef) ||
|
|
110
|
+
input.contentFingerprint !== input.bootstrapRef)
|
|
111
|
+
return false;
|
|
112
|
+
const identity = parseProductDiscussionConversationLocator(input.conversationLocator);
|
|
113
|
+
if (!identity ||
|
|
114
|
+
identity.roleRef !== input.roleRef ||
|
|
115
|
+
identity.conversationRef !== input.conversationRef ||
|
|
116
|
+
productDiscussionRef(identity) !== input.discussionRef)
|
|
117
|
+
return false;
|
|
118
|
+
await assertProductDiscussionConversationPreTask({
|
|
119
|
+
conversationLocator: input.conversationLocator,
|
|
120
|
+
listTaskIds: options.listTaskIds,
|
|
121
|
+
async getTaskRoleBindings(taskId) {
|
|
122
|
+
return (await options.getTaskFact(taskId)).roleBindings;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const session = await options.getDiscussionSession(input.discussionRef);
|
|
126
|
+
if (session.status !== "ACTIVE" ||
|
|
127
|
+
session.discussionRef !== input.discussionRef ||
|
|
128
|
+
session.roleRef !== input.roleRef ||
|
|
129
|
+
session.conversationRef !== input.conversationRef ||
|
|
130
|
+
session.conversationLocator !== input.conversationLocator ||
|
|
131
|
+
session.generation !== input.discussionGeneration)
|
|
132
|
+
return false;
|
|
133
|
+
const current = authority.get(input.discussionRef);
|
|
134
|
+
if (!current ||
|
|
135
|
+
current.bootstrapRef !== input.bootstrapRef ||
|
|
136
|
+
current.contentInstanceId !== input.contentInstanceId)
|
|
137
|
+
return false;
|
|
138
|
+
return authority.verify({
|
|
139
|
+
discussionRef: input.discussionRef,
|
|
140
|
+
roleRef: input.roleRef,
|
|
141
|
+
conversationRef: input.conversationRef,
|
|
142
|
+
conversationLocator: input.conversationLocator,
|
|
143
|
+
generation: input.discussionGeneration,
|
|
144
|
+
discussionAdmissionToken: input.discussionAdmissionToken,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
recoverReservations,
|
|
153
|
+
reserveTaskWorker,
|
|
154
|
+
releaseTaskWorker,
|
|
155
|
+
isTaskWorkerProvisioning,
|
|
156
|
+
observe: coordinator.observe,
|
|
157
|
+
authorizeBootstrapExecution,
|
|
158
|
+
verifyAdmission(input) {
|
|
159
|
+
return recovered && !isTaskWorkerProvisioning(input.roleRef) && authority.verify(input);
|
|
160
|
+
},
|
|
161
|
+
clear() {
|
|
162
|
+
authority.clear();
|
|
163
|
+
provisioning.clear();
|
|
164
|
+
recovered = false;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -33,5 +33,6 @@ export declare function createReconciliationCoordinator(options: ReconciliationC
|
|
|
33
33
|
kick(taskId: string, signal?: TaskResumeSignal): void;
|
|
34
34
|
reconcile: (taskId: string, signal?: TaskResumeSignal) => Promise<void>;
|
|
35
35
|
sweep: () => Promise<void>;
|
|
36
|
+
drain(): Promise<void>;
|
|
36
37
|
stop(): void;
|
|
37
38
|
}>;
|
|
@@ -197,8 +197,7 @@ export function createReconciliationCoordinator(options) {
|
|
|
197
197
|
}
|
|
198
198
|
})().finally(() => {
|
|
199
199
|
taskInFlight.delete(taskId);
|
|
200
|
-
if (!stopped &&
|
|
201
|
-
(pendingSignals.has(taskId) || failures.has(taskId)))
|
|
200
|
+
if (!stopped && (pendingSignals.has(taskId) || failures.has(taskId)))
|
|
202
201
|
schedulePendingRetry(taskId);
|
|
203
202
|
else
|
|
204
203
|
admittedTasks.delete(taskId);
|
|
@@ -318,6 +317,12 @@ export function createReconciliationCoordinator(options) {
|
|
|
318
317
|
},
|
|
319
318
|
reconcile,
|
|
320
319
|
sweep,
|
|
320
|
+
async drain() {
|
|
321
|
+
await Promise.allSettled([
|
|
322
|
+
...(sweepInFlight ? [sweepInFlight] : []),
|
|
323
|
+
...taskInFlight.values(),
|
|
324
|
+
]);
|
|
325
|
+
},
|
|
321
326
|
stop() {
|
|
322
327
|
if (stopped)
|
|
323
328
|
return;
|
|
@@ -42,6 +42,14 @@ export const roleOperations = {
|
|
|
42
42
|
"getTask",
|
|
43
43
|
"putTaskDocument",
|
|
44
44
|
"getTaskDocument",
|
|
45
|
+
"getProductCampaign",
|
|
46
|
+
"getProductDiscussionContext",
|
|
47
|
+
"getProductDocument",
|
|
48
|
+
"putProductDocument",
|
|
49
|
+
"getCampaignAuthorization",
|
|
50
|
+
"submitProductTaskIntent",
|
|
51
|
+
"recordProductGapReview",
|
|
52
|
+
"recordProductGoalSatisfied",
|
|
45
53
|
"askPeer",
|
|
46
54
|
"replyPeer",
|
|
47
55
|
...directToolActionIds,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-platform-host",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "4.1.12",
|
|
27
|
-
"@tomflow/proflow-
|
|
28
|
-
"@tomflow/proflow-task-migration-runner": "^0.1.11",
|
|
29
|
-
"@tomflow/proflow-task-orchestration": "^0.1.11",
|
|
30
|
-
"@tomflow/proflow-task-store-sqlite": "^0.1.12",
|
|
31
|
-
"@tomflow/proflow-agent-runtime": "^0.1.17",
|
|
27
|
+
"@tomflow/proflow-agent-runtime": "^0.1.18",
|
|
32
28
|
"@tomflow/proflow-module-contract": "^0.1.13",
|
|
33
|
-
"@tomflow/proflow-
|
|
29
|
+
"@tomflow/proflow-task-migration-runner": "^0.1.11",
|
|
30
|
+
"@tomflow/proflow-execution-browser-extension": "^0.1.74",
|
|
31
|
+
"@tomflow/proflow-execution-contracts": "^0.1.12",
|
|
32
|
+
"@tomflow/proflow-task-orchestration": "^0.1.12",
|
|
33
|
+
"@tomflow/proflow-task-store-sqlite": "^0.1.12"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tomflow/proflow-
|
|
37
|
-
"@tomflow/proflow-execution-runtime": "^0.1.
|
|
38
|
-
"@tomflow/proflow-model-runtime": "^0.1.
|
|
39
|
-
"@tomflow/proflow-
|
|
36
|
+
"@tomflow/proflow-agent-gateway": "^0.1.19",
|
|
37
|
+
"@tomflow/proflow-execution-runtime": "^0.1.20",
|
|
38
|
+
"@tomflow/proflow-model-runtime": "^0.1.26",
|
|
39
|
+
"@tomflow/proflow-deployment-conformance": "^0.1.13"
|
|
40
40
|
},
|
|
41
41
|
"description": "Provides the ProFlow local application composition root that binds Task, Agent, Execution and Model owner transports.",
|
|
42
42
|
"keywords": [
|
package/proflow.module.json
CHANGED