pi-ultracode 0.3.2 → 0.5.0
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 +32 -14
- package/README.zh-CN.md +32 -14
- package/extensions/ultracode.ts +11 -10
- package/package.json +17 -7
- package/src/commands.ts +21 -22
- package/src/depth.ts +38 -0
- package/src/index.ts +9 -1
- package/src/mode.ts +82 -58
- package/src/prompts.ts +65 -32
- package/src/thinking.ts +1 -1
- package/src/workflow/agent-runner.ts +23 -8
- package/src/workflow/agent-types.ts +19 -15
- package/src/workflow/hermetic-git.ts +31 -0
- package/src/workflow/journal.ts +134 -3
- package/src/workflow/parser.ts +254 -38
- package/src/workflow/registry.ts +44 -67
- package/src/workflow/repository-context.ts +76 -0
- package/src/workflow/run-artifacts.ts +60 -14
- package/src/workflow/runtime.ts +380 -180
- package/src/workflow/saved-workflow.ts +37 -0
- package/src/workflow/script-executor.ts +56 -10
- package/src/workflow/script-worker.mjs +65 -19
- package/src/workflow/tool.ts +108 -58
- package/src/workflow/workflow-overlay.ts +4 -4
- package/src/workflow/worktree.ts +1965 -244
- package/types/workflow.d.ts +2 -4
package/src/workflow/parser.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface ParsedWorkflow {
|
|
|
33
33
|
export const WORKFLOW_CHECKPOINT_IDENTIFIER = "__ultracodeCheckpoint";
|
|
34
34
|
const WORKFLOW_LOOP_IDENTIFIER = "__ultracodeLoop";
|
|
35
35
|
const WORKFLOW_INVOKE_IDENTIFIER = "__ultracodeInvoke";
|
|
36
|
+
const WORKFLOW_SAFE_MAP_IDENTIFIER = "__ultracodeMapThunks";
|
|
36
37
|
const WORKFLOW_INTERNAL_CALLS = {
|
|
37
38
|
agent: "__ultracodeAgent",
|
|
38
39
|
parallel: "__ultracodeParallel",
|
|
@@ -43,6 +44,7 @@ const WORKFLOW_RESERVED_IDENTIFIERS = new Set([
|
|
|
43
44
|
WORKFLOW_CHECKPOINT_IDENTIFIER,
|
|
44
45
|
WORKFLOW_LOOP_IDENTIFIER,
|
|
45
46
|
WORKFLOW_INVOKE_IDENTIFIER,
|
|
47
|
+
WORKFLOW_SAFE_MAP_IDENTIFIER,
|
|
46
48
|
...Object.values(WORKFLOW_INTERNAL_CALLS),
|
|
47
49
|
]);
|
|
48
50
|
export const DEFAULT_WORKFLOW_CHECKPOINT_LIMIT = 1_000_000;
|
|
@@ -249,10 +251,18 @@ function assertDeterministicAst(
|
|
|
249
251
|
if (isUnsupportedClassEagerOrchestration(node, orchestrationBindings)) {
|
|
250
252
|
throw new Error("Workflow classes may call orchestration only from ordinary methods, not constructors, fields, or static blocks.");
|
|
251
253
|
}
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
+
if (
|
|
255
|
+
isUnsupportedFactoryProperty(node)
|
|
256
|
+
|| isUnsupportedAccessorForwarding(node, orchestrationBindings)
|
|
257
|
+
|| isAmbiguousOrchestrationContainer(node)
|
|
258
|
+
) {
|
|
259
|
+
throw new Error("Workflow orchestration helper properties must use unique static methods without spreads; accessor forwarding is unsupported.");
|
|
254
260
|
}
|
|
255
|
-
if (
|
|
261
|
+
if (
|
|
262
|
+
isUnsupportedCompositeFactory(node, orchestrationBindings)
|
|
263
|
+
|| isUnsupportedAnonymousHelperExtraction(node)
|
|
264
|
+
|| isUnsupportedHelperDestructuringAssignment(node, orchestrationBindings)
|
|
265
|
+
) {
|
|
256
266
|
throw new Error("Workflow orchestration helper factories must use a direct function, object, or class declaration.");
|
|
257
267
|
}
|
|
258
268
|
if (isUnsupportedHelperAssignment(node, orchestrationBindings)) {
|
|
@@ -267,10 +277,11 @@ function assertDeterministicAst(
|
|
|
267
277
|
isDynamicMethodCall(node)
|
|
268
278
|
|| isDynamicOrchestrationMember(node, orchestrationBindings)
|
|
269
279
|
|| isOrchestrationDestructure(node, orchestrationBindings)
|
|
270
|
-
|| isOrchestrationArgumentEscape(node, orchestrationBindings)
|
|
280
|
+
|| isOrchestrationArgumentEscape(node, orchestrationBindings, parent)
|
|
271
281
|
|| isOrchestrationFunctionAlias(node, parent)
|
|
272
282
|
|| isOrchestrationMemberAlias(node, parent, orchestrationBindings)
|
|
273
283
|
|| isOrchestrationBindingEscape(node, parent, orchestrationBindings)
|
|
284
|
+
|| isTaintedTaggedCall(node, orchestrationBindings)
|
|
274
285
|
) {
|
|
275
286
|
throw new Error(
|
|
276
287
|
"Workflow scripts must use a static method name; dynamic method calls can bypass deterministic promise checks.",
|
|
@@ -279,6 +290,9 @@ function assertDeterministicAst(
|
|
|
279
290
|
if (node.type === "Identifier" && node.name === "globalThis") {
|
|
280
291
|
throw new Error("Workflow scripts must not access globalThis; use the declared workflow globals directly.");
|
|
281
292
|
}
|
|
293
|
+
if (node.type === "Identifier" && node.name === "process" && isIdentifierReference(node, parent)) {
|
|
294
|
+
throw new Error("Workflow scripts do not expose process; use the declared cwd global directly.");
|
|
295
|
+
}
|
|
282
296
|
if (
|
|
283
297
|
node.type === "MemberExpression"
|
|
284
298
|
&& node.object?.type === "ThisExpression"
|
|
@@ -335,10 +349,21 @@ function collectCallSiteEdits(
|
|
|
335
349
|
): void {
|
|
336
350
|
const orchestrationBindings = collectOrchestrationBindings(root);
|
|
337
351
|
const localFunctions = collectLocalFunctionNames(root, orchestrationBindings);
|
|
338
|
-
const stack: Array<{ node: AnyNode; loops: string[] }> = [{ node: root, loops: [] }];
|
|
352
|
+
const stack: Array<{ node: AnyNode; parent?: AnyNode; loops: string[] }> = [{ node: root, loops: [] }];
|
|
339
353
|
while (stack.length > 0) {
|
|
340
|
-
const { node, loops } = stack.pop()!;
|
|
341
|
-
if (
|
|
354
|
+
const { node, parent, loops } = stack.pop()!;
|
|
355
|
+
if (isSafeParallelMap(node, parent, orchestrationBindings)) {
|
|
356
|
+
const openOffset = source.slice(node.callee.end, node.end).indexOf("(");
|
|
357
|
+
if (openOffset < 0) throw new Error("unable to instrument safe parallel map");
|
|
358
|
+
const openParen = node.callee.end + openOffset;
|
|
359
|
+
const receiver = source.slice(node.callee.object.start, node.callee.object.end);
|
|
360
|
+
replacements.push({
|
|
361
|
+
start: node.callee.start,
|
|
362
|
+
end: node.callee.end,
|
|
363
|
+
text: WORKFLOW_SAFE_MAP_IDENTIFIER,
|
|
364
|
+
});
|
|
365
|
+
insertions.push({ pos: openParen + 1, text: `${receiver},` });
|
|
366
|
+
} else if (
|
|
342
367
|
node.type === "CallExpression"
|
|
343
368
|
&& node.callee?.type === "Identifier"
|
|
344
369
|
&& Object.hasOwn(WORKFLOW_INTERNAL_CALLS, node.callee.name)
|
|
@@ -359,7 +384,8 @@ function collectCallSiteEdits(
|
|
|
359
384
|
text: `${JSON.stringify(callSite)}${node.arguments.length > 0 ? "," : ""}`,
|
|
360
385
|
});
|
|
361
386
|
} else if (node.type === "CallExpression" && (
|
|
362
|
-
(node.callee?.type === "Identifier"
|
|
387
|
+
(node.callee?.type === "Identifier"
|
|
388
|
+
&& (localFunctions.has(node.callee.name) || orchestrationBindings.has(node.callee.name)))
|
|
363
389
|
|| (node.callee?.type === "MemberExpression"
|
|
364
390
|
&& isTaintedStaticMember(node.callee, orchestrationBindings))
|
|
365
391
|
)) {
|
|
@@ -394,6 +420,7 @@ function collectCallSiteEdits(
|
|
|
394
420
|
for (const child of astChildren(node)) {
|
|
395
421
|
stack.push({
|
|
396
422
|
node: child,
|
|
423
|
+
parent: node,
|
|
397
424
|
loops: body && loopSite && child === body ? [...loops, loopSite] : loops,
|
|
398
425
|
});
|
|
399
426
|
}
|
|
@@ -521,8 +548,8 @@ function collectOrchestrationBindings(root: AnyNode): Set<string> {
|
|
|
521
548
|
? containsDirectOrchestration(definition.value)
|
|
522
549
|
: containsUnconsumedOrchestration(definition.value);
|
|
523
550
|
if (tainted) seeds.add(definition.name);
|
|
524
|
-
if (definition.value.type === "ObjectExpression") {
|
|
525
|
-
for (const member of
|
|
551
|
+
if (definition.value.type === "ObjectExpression" || definition.value.type === "ArrayExpression") {
|
|
552
|
+
for (const member of containerFactoryMembers(definition.name, definition.value)) addMemberSeed(seeds, member);
|
|
526
553
|
}
|
|
527
554
|
for (const alias of collectStaticAliases(definition.name, definition.value)) {
|
|
528
555
|
const targets = aliases.get(alias.source) ?? new Set<string>();
|
|
@@ -620,18 +647,28 @@ function collectReferencedIdentifiers(root: AnyNode): Set<string> {
|
|
|
620
647
|
return references;
|
|
621
648
|
}
|
|
622
649
|
|
|
623
|
-
function
|
|
650
|
+
function containerFactoryMembers(name: string, node: AnyNode): string[] {
|
|
624
651
|
const members: string[] = [];
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
652
|
+
const entries: Array<{ key: string; value: AnyNode }> = [];
|
|
653
|
+
if (node.type === "ObjectExpression") {
|
|
654
|
+
for (const property of node.properties ?? []) {
|
|
655
|
+
if (property.type !== "Property") continue;
|
|
656
|
+
const key = property.computed
|
|
657
|
+
? staticStringOf(property.key)
|
|
658
|
+
: property.key?.name ?? (typeof property.key?.value === "string" ? property.key.value : undefined);
|
|
659
|
+
if (key !== undefined) entries.push({ key, value: property.value });
|
|
660
|
+
}
|
|
661
|
+
} else if (node.type === "ArrayExpression") {
|
|
662
|
+
for (let index = 0; index < (node.elements?.length ?? 0); index++) {
|
|
663
|
+
const value = node.elements[index];
|
|
664
|
+
if (value && value.type !== "SpreadElement") entries.push({ key: String(index), value });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const { key, value } of entries) {
|
|
631
668
|
const member = `${name}.${key}`;
|
|
632
|
-
if (isFunctionNode(
|
|
633
|
-
if (
|
|
634
|
-
members.push(...
|
|
669
|
+
if (isFunctionNode(value) && containsDirectOrchestration(value)) members.push(member);
|
|
670
|
+
if (value.type === "ObjectExpression" || value.type === "ArrayExpression") {
|
|
671
|
+
members.push(...containerFactoryMembers(member, value));
|
|
635
672
|
}
|
|
636
673
|
}
|
|
637
674
|
return members;
|
|
@@ -759,7 +796,11 @@ function isDynamicOrchestrationMember(node: AnyNode, bindings: ReadonlySet<strin
|
|
|
759
796
|
&& isOrchestrationExpression(node.object, bindings);
|
|
760
797
|
}
|
|
761
798
|
|
|
762
|
-
function isOrchestrationArgumentEscape(
|
|
799
|
+
function isOrchestrationArgumentEscape(
|
|
800
|
+
node: AnyNode,
|
|
801
|
+
bindings: ReadonlySet<string>,
|
|
802
|
+
parent?: AnyNode,
|
|
803
|
+
): boolean {
|
|
763
804
|
if (node.type === "TaggedTemplateExpression") {
|
|
764
805
|
return (node.quasi?.expressions ?? []).some((expression: AnyNode) =>
|
|
765
806
|
isOrchestrationExpression(expression, bindings)
|
|
@@ -769,19 +810,98 @@ function isOrchestrationArgumentEscape(node: AnyNode, bindings: ReadonlySet<stri
|
|
|
769
810
|
node.type === "CallExpression"
|
|
770
811
|
&& node.callee?.type === "Identifier"
|
|
771
812
|
&& ORCHESTRATION_FUNCTIONS.has(node.callee.name)
|
|
772
|
-
)
|
|
813
|
+
) {
|
|
814
|
+
return node.arguments.some((argument: AnyNode) =>
|
|
815
|
+
containsEagerOrchestration(
|
|
816
|
+
argument.type === "SpreadElement" ? argument.argument : argument,
|
|
817
|
+
bindings,
|
|
818
|
+
));
|
|
819
|
+
}
|
|
820
|
+
if (isSafeParallelMap(node, parent, bindings)) return false;
|
|
773
821
|
return (node.type === "CallExpression" || node.type === "NewExpression")
|
|
774
|
-
&& node.arguments.some((argument: AnyNode) =>
|
|
775
|
-
argument.type === "SpreadElement"
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
);
|
|
822
|
+
&& node.arguments.some((argument: AnyNode) => {
|
|
823
|
+
const value = argument.type === "SpreadElement" ? argument.argument : argument;
|
|
824
|
+
return isOrchestrationExpression(value, bindings)
|
|
825
|
+
|| containsEscapingOrchestration(value, bindings);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function containsEagerOrchestration(root: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
830
|
+
const stack = [root];
|
|
831
|
+
while (stack.length > 0) {
|
|
832
|
+
const node = stack.pop()!;
|
|
833
|
+
if (isFunctionNode(node)) continue;
|
|
834
|
+
if (node.type === "CallExpression" && isOrchestrationExpression(node, bindings)) return true;
|
|
835
|
+
if (node.type === "Identifier" && bindings.has(node.name)) return true;
|
|
836
|
+
if (node.type === "MemberExpression" && isTaintedStaticMember(node, bindings)) return true;
|
|
837
|
+
stack.push(...astChildren(node));
|
|
838
|
+
}
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Find raw orchestration capabilities passed to another call; awaited results are plain data. */
|
|
843
|
+
function containsEscapingOrchestration(root: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
844
|
+
const stack: Array<{ node: AnyNode; parent?: AnyNode }> = [{ node: root }];
|
|
845
|
+
while (stack.length > 0) {
|
|
846
|
+
const { node, parent } = stack.pop()!;
|
|
847
|
+
if (node.type === "AwaitExpression") continue;
|
|
848
|
+
if (isFunctionNode(node) && containsOrchestrationCall(node, bindings)) return true;
|
|
849
|
+
if (node.type === "CallExpression" && isOrchestrationExpression(node, bindings)) return true;
|
|
850
|
+
if (node.type === "MemberExpression" && propertyNameOf(node) !== undefined) {
|
|
851
|
+
if (isOrchestrationExpression(node, bindings)) return true;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (
|
|
855
|
+
node.type === "Identifier"
|
|
856
|
+
&& isIdentifierReference(node, parent)
|
|
857
|
+
&& (bindings.has(node.name) || hasTaintedMember(bindings, node.name))
|
|
858
|
+
) return true;
|
|
859
|
+
for (const child of astChildren(node)) stack.push({ node: child, parent: node });
|
|
860
|
+
}
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function isSafeParallelMap(
|
|
865
|
+
node: AnyNode,
|
|
866
|
+
parent: AnyNode | undefined,
|
|
867
|
+
bindings: ReadonlySet<string>,
|
|
868
|
+
): boolean {
|
|
869
|
+
if (
|
|
870
|
+
node.type !== "CallExpression"
|
|
871
|
+
|| node.callee?.type !== "MemberExpression"
|
|
872
|
+
|| propertyNameOf(node.callee) !== "map"
|
|
873
|
+
|| parent?.type !== "CallExpression"
|
|
874
|
+
|| parent.callee?.type !== "Identifier"
|
|
875
|
+
|| parent.callee.name !== "parallel"
|
|
876
|
+
|| !parent.arguments.includes(node)
|
|
877
|
+
|| node.arguments?.length !== 1
|
|
878
|
+
|| containsOrchestrationCall(node.callee.object, bindings)
|
|
879
|
+
) return false;
|
|
880
|
+
const mapper = node.arguments[0];
|
|
881
|
+
if (
|
|
882
|
+
!mapper
|
|
883
|
+
|| (mapper.type !== "ArrowFunctionExpression" && mapper.type !== "FunctionExpression")
|
|
884
|
+
|| mapper.async
|
|
885
|
+
|| mapper.generator
|
|
886
|
+
|| (mapper.params ?? []).some((parameter: AnyNode) => containsOrchestrationCall(parameter, bindings))
|
|
887
|
+
) return false;
|
|
888
|
+
const expressions = mapper.body?.type === "SequenceExpression"
|
|
889
|
+
? mapper.body.expressions
|
|
890
|
+
: [mapper.body];
|
|
891
|
+
const thunk = expressions?.at(-1);
|
|
892
|
+
if (!isFunctionNode(thunk)) return false;
|
|
893
|
+
return expressions.slice(0, -1).every((expression: AnyNode) =>
|
|
894
|
+
!containsOrchestrationCall(expression, bindings));
|
|
779
895
|
}
|
|
780
896
|
|
|
781
897
|
function isUnsupportedHelperCall(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
782
898
|
if (node.type !== "CallExpression") return false;
|
|
783
899
|
if (node.callee?.type === "MemberExpression") {
|
|
784
|
-
if (
|
|
900
|
+
if (
|
|
901
|
+
node.callee.object?.type === "ThisExpression"
|
|
902
|
+
|| node.callee.object?.type === "AwaitExpression"
|
|
903
|
+
|| node.callee.object?.type === "Super"
|
|
904
|
+
) return true;
|
|
785
905
|
return (node.callee.object?.type === "CallExpression" || node.callee.object?.type === "NewExpression")
|
|
786
906
|
&& isOrchestrationExpression(node.callee.object, bindings);
|
|
787
907
|
}
|
|
@@ -844,10 +964,97 @@ function isUnsupportedAccessorForwarding(node: AnyNode, bindings: ReadonlySet<st
|
|
|
844
964
|
return containsDirectOrchestration(value);
|
|
845
965
|
}
|
|
846
966
|
|
|
847
|
-
function
|
|
967
|
+
function isAmbiguousOrchestrationContainer(node: AnyNode): boolean {
|
|
968
|
+
if (node.type !== "ObjectExpression" || !containsDirectOrchestration(node)) return false;
|
|
969
|
+
const seen = new Set<string>();
|
|
970
|
+
for (const property of node.properties ?? []) {
|
|
971
|
+
if (property.type !== "Property") return true;
|
|
972
|
+
const key = property.computed ? staticStringOf(property.key) : propertyKeyName(property.key);
|
|
973
|
+
if (key === undefined || key === "__proto__" || seen.has(key)) return true;
|
|
974
|
+
seen.add(key);
|
|
975
|
+
}
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function isUnsupportedCompositeFactory(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
848
980
|
if (node.type !== "VariableDeclarator" || !node.init) return false;
|
|
849
|
-
|
|
850
|
-
|
|
981
|
+
if (["ConditionalExpression", "LogicalExpression", "SequenceExpression"].includes(node.init.type)) {
|
|
982
|
+
return containsDirectOrchestration(node.init);
|
|
983
|
+
}
|
|
984
|
+
const init = node.init.type === "ChainExpression" ? node.init.expression : node.init;
|
|
985
|
+
if (init.type !== "CallExpression") return false;
|
|
986
|
+
if (init.callee?.type === "Identifier") return bindings.has(init.callee.name);
|
|
987
|
+
if (isFunctionNode(init.callee)) return containsOrchestrationCall(init.callee, bindings);
|
|
988
|
+
if (init.callee?.type !== "MemberExpression") {
|
|
989
|
+
return containsOrchestrationCall(init.callee, bindings)
|
|
990
|
+
|| isOrchestrationExpression(init.callee, bindings);
|
|
991
|
+
}
|
|
992
|
+
if (isTaintedStaticMember(init.callee, bindings)) return true;
|
|
993
|
+
return init.callee.object?.type === "Identifier"
|
|
994
|
+
&& hasTaintedMember(bindings, init.callee.object.name);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function isUnsupportedAnonymousHelperExtraction(node: AnyNode): boolean {
|
|
998
|
+
if (node.type !== "VariableDeclarator" || !node.init) return false;
|
|
999
|
+
const init = node.init.type === "ChainExpression" ? node.init.expression : node.init;
|
|
1000
|
+
if (node.id?.type === "ObjectPattern" || node.id?.type === "ArrayPattern") {
|
|
1001
|
+
return destructureSelectsOrchestration(node.id, init);
|
|
1002
|
+
}
|
|
1003
|
+
if (node.id?.type !== "Identifier" || init.type !== "MemberExpression") return false;
|
|
1004
|
+
const selected = selectedLiteralMember(init);
|
|
1005
|
+
return selected ? containsDirectOrchestration(selected) : containsDirectOrchestration(init.object);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function isUnsupportedHelperDestructuringAssignment(
|
|
1009
|
+
node: AnyNode,
|
|
1010
|
+
bindings: ReadonlySet<string>,
|
|
1011
|
+
): boolean {
|
|
1012
|
+
return node.type === "AssignmentExpression"
|
|
1013
|
+
&& (node.left?.type === "ObjectPattern" || node.left?.type === "ArrayPattern")
|
|
1014
|
+
&& (
|
|
1015
|
+
destructureSelectsOrchestration(node.left, node.right)
|
|
1016
|
+
|| isOrchestrationExpression(node.right, bindings)
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function destructureSelectsOrchestration(pattern: AnyNode, value: AnyNode): boolean {
|
|
1021
|
+
if (pattern.type === "ObjectPattern" && value.type === "ObjectExpression") {
|
|
1022
|
+
for (const selected of pattern.properties ?? []) {
|
|
1023
|
+
if (selected.type !== "Property") return true;
|
|
1024
|
+
const key = propertyKeyName(selected.key);
|
|
1025
|
+
if (key === undefined) return true;
|
|
1026
|
+
const match = findLastStaticProperty(value.properties ?? [], key);
|
|
1027
|
+
if (match && containsDirectOrchestration(match.value)) return true;
|
|
1028
|
+
}
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
if (pattern.type === "ArrayPattern" && value.type === "ArrayExpression") {
|
|
1032
|
+
return (pattern.elements ?? []).some((selected: AnyNode | null, index: number) =>
|
|
1033
|
+
!!selected && !!value.elements?.[index] && containsDirectOrchestration(value.elements[index]));
|
|
1034
|
+
}
|
|
1035
|
+
return containsDirectOrchestration(value);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function selectedLiteralMember(member: AnyNode): AnyNode | undefined {
|
|
1039
|
+
const key = propertyNameOf(member);
|
|
1040
|
+
if (key === undefined) return undefined;
|
|
1041
|
+
if (member.object?.type === "ObjectExpression") {
|
|
1042
|
+
return findLastStaticProperty(member.object.properties ?? [], key)?.value;
|
|
1043
|
+
}
|
|
1044
|
+
if (member.object?.type === "ArrayExpression" && /^\d+$/.test(key)) {
|
|
1045
|
+
return member.object.elements?.[Number(key)] ?? undefined;
|
|
1046
|
+
}
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function findLastStaticProperty(properties: AnyNode[], key: string): AnyNode | undefined {
|
|
1051
|
+
for (let index = properties.length - 1; index >= 0; index--) {
|
|
1052
|
+
const property = properties[index];
|
|
1053
|
+
if (property.type !== "Property") continue;
|
|
1054
|
+
const candidate = property.computed ? staticStringOf(property.key) : propertyKeyName(property.key);
|
|
1055
|
+
if (candidate === key) return property;
|
|
1056
|
+
}
|
|
1057
|
+
return undefined;
|
|
851
1058
|
}
|
|
852
1059
|
|
|
853
1060
|
function isUnsupportedHelperAssignment(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
@@ -886,6 +1093,12 @@ function isOrchestrationBindingEscape(
|
|
|
886
1093
|
}
|
|
887
1094
|
|
|
888
1095
|
function isTaintedStaticMember(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
1096
|
+
const selected = selectedLiteralMember(node);
|
|
1097
|
+
if (selected) {
|
|
1098
|
+
return isFunctionNode(selected)
|
|
1099
|
+
? containsOrchestrationCall(selected, bindings)
|
|
1100
|
+
: isOrchestrationExpression(selected, bindings);
|
|
1101
|
+
}
|
|
889
1102
|
const member = staticMemberReference(node);
|
|
890
1103
|
if (!member) return isOrchestrationExpression(node.object, bindings);
|
|
891
1104
|
if (bindings.has(member) || hasTaintedMember(bindings, member)) return true;
|
|
@@ -933,6 +1146,14 @@ function isForbiddenPromiseDestructure(node: AnyNode, parent?: AnyNode): boolean
|
|
|
933
1146
|
&& ["then", "catch", "finally"].includes(propertyNameOf(node) ?? propertyKeyName(node.key) ?? "");
|
|
934
1147
|
}
|
|
935
1148
|
|
|
1149
|
+
function isTaintedTaggedCall(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
1150
|
+
if (node.type !== "TaggedTemplateExpression") return false;
|
|
1151
|
+
if (node.tag?.type === "Identifier") return bindings.has(node.tag.name);
|
|
1152
|
+
if (node.tag?.type === "MemberExpression") return isTaintedStaticMember(node.tag, bindings);
|
|
1153
|
+
return node.tag?.type === "ChainExpression"
|
|
1154
|
+
&& isOrchestrationExpression(node.tag.expression, bindings);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
936
1157
|
function isDynamicMethodCall(node: AnyNode): boolean {
|
|
937
1158
|
return node.type === "CallExpression"
|
|
938
1159
|
&& node.callee?.type === "MemberExpression"
|
|
@@ -953,13 +1174,8 @@ function isForbiddenPromiseCall(node: AnyNode): boolean {
|
|
|
953
1174
|
&& ["all", "allSettled", "race", "any"].includes(propertyNameOf(node.callee) ?? "");
|
|
954
1175
|
}
|
|
955
1176
|
|
|
956
|
-
function isForbiddenPromiseMember(node: AnyNode,
|
|
957
|
-
|
|
958
|
-
const property = propertyNameOf(node);
|
|
959
|
-
if (property === "Promise") return true;
|
|
960
|
-
return ["all", "allSettled", "race", "any"].includes(property ?? "")
|
|
961
|
-
&& parent?.type === "CallExpression"
|
|
962
|
-
&& parent.callee === node;
|
|
1177
|
+
function isForbiddenPromiseMember(node: AnyNode, _parent?: AnyNode): boolean {
|
|
1178
|
+
return node.type === "MemberExpression" && propertyNameOf(node) === "Promise";
|
|
963
1179
|
}
|
|
964
1180
|
|
|
965
1181
|
function isAllowedPromiseReference(node: AnyNode, parent?: AnyNode, grandparent?: AnyNode): boolean {
|
package/src/workflow/registry.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process-local registry
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Process-local registry for one Pi extension session.
|
|
3
|
+
*
|
|
4
|
+
* A registry instance is permanently scoped by ownership: callers create one per
|
|
5
|
+
* extension/session and inject that same instance into the workflow tool and
|
|
6
|
+
* commands. No mutable process-global "current session" exists.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import * as fs from "node:fs";
|
|
@@ -16,15 +18,10 @@ export interface RunHandle {
|
|
|
16
18
|
details?: WorkflowRunDetails;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
interface RegistryScope {
|
|
20
|
-
runs: Map<string, RunHandle>;
|
|
21
|
-
order: string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
export class WorkflowRegistry {
|
|
25
|
-
private readonly
|
|
22
|
+
private readonly runs = new Map<string, RunHandle>();
|
|
23
|
+
private order: string[] = [];
|
|
26
24
|
private readonly listeners = new Set<() => void>();
|
|
27
|
-
private scopeDir?: string;
|
|
28
25
|
|
|
29
26
|
register(
|
|
30
27
|
runId: string,
|
|
@@ -32,13 +29,11 @@ export class WorkflowRegistry {
|
|
|
32
29
|
abort: () => void,
|
|
33
30
|
details?: WorkflowRunDetails,
|
|
34
31
|
): RunHandle {
|
|
35
|
-
const scope = this.currentScope();
|
|
36
32
|
const handle: RunHandle = { snapshot, abort, startedAt: Date.now(), details };
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.trimRuns(scope);
|
|
33
|
+
this.runs.set(runId, handle);
|
|
34
|
+
this.order = this.order.filter((id) => id !== runId);
|
|
35
|
+
this.order.push(runId);
|
|
36
|
+
this.trimRuns();
|
|
42
37
|
this.notify();
|
|
43
38
|
return handle;
|
|
44
39
|
}
|
|
@@ -58,10 +53,8 @@ export class WorkflowRegistry {
|
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
|
|
61
|
-
/** Restore the newest task-detail manifests
|
|
56
|
+
/** Restore the newest task-detail manifests owned by this registry session. */
|
|
62
57
|
restoreRuns(runsDir: string): number {
|
|
63
|
-
this.setScope(runsDir);
|
|
64
|
-
const scope = this.currentScope();
|
|
65
58
|
let files: string[];
|
|
66
59
|
try {
|
|
67
60
|
files = fs.readdirSync(runsDir)
|
|
@@ -72,10 +65,11 @@ export class WorkflowRegistry {
|
|
|
72
65
|
} catch {
|
|
73
66
|
return 0;
|
|
74
67
|
}
|
|
68
|
+
|
|
75
69
|
let restored = 0;
|
|
76
70
|
for (const file of files) {
|
|
77
71
|
const loaded = WorkflowRunDetails.restore(file);
|
|
78
|
-
if (!loaded ||
|
|
72
|
+
if (!loaded || this.runs.has(loaded.snapshot.runId ?? loaded.details.runId)) continue;
|
|
79
73
|
const runId = loaded.snapshot.runId ?? loaded.details.runId;
|
|
80
74
|
if (loaded.snapshot.status === "running") {
|
|
81
75
|
loaded.snapshot.status = "aborted";
|
|
@@ -83,7 +77,10 @@ export class WorkflowRegistry {
|
|
|
83
77
|
if (agent.status !== "running") continue;
|
|
84
78
|
agent.status = "cancelled";
|
|
85
79
|
agent.currentTurn = undefined;
|
|
86
|
-
loaded.details.finishTask(agent.id, {
|
|
80
|
+
loaded.details.finishTask(agent.id, {
|
|
81
|
+
status: "cancelled",
|
|
82
|
+
error: "Session ended before the task completed.",
|
|
83
|
+
});
|
|
87
84
|
}
|
|
88
85
|
loaded.snapshot = recompute(loaded.snapshot);
|
|
89
86
|
loaded.details.persist(loaded.snapshot);
|
|
@@ -94,85 +91,65 @@ export class WorkflowRegistry {
|
|
|
94
91
|
} catch {
|
|
95
92
|
// Keep a deterministic in-memory fallback if the manifest disappears.
|
|
96
93
|
}
|
|
97
|
-
|
|
94
|
+
this.runs.set(runId, {
|
|
98
95
|
snapshot: loaded.snapshot,
|
|
99
96
|
abort: () => {},
|
|
100
97
|
startedAt,
|
|
101
98
|
details: loaded.details,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
scope.order.push(runId);
|
|
99
|
+
});
|
|
100
|
+
this.order.push(runId);
|
|
105
101
|
restored++;
|
|
106
102
|
}
|
|
107
|
-
this.trimRuns(
|
|
103
|
+
this.trimRuns();
|
|
108
104
|
if (restored) this.notify();
|
|
109
105
|
return restored;
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
/** Select a session without discarding live handles from other sessions. */
|
|
113
|
-
setScope(runsDir: string): void {
|
|
114
|
-
const resolved = path.resolve(runsDir);
|
|
115
|
-
if (this.scopeDir === resolved) return;
|
|
116
|
-
this.scopeDir = resolved;
|
|
117
|
-
this.currentScope();
|
|
118
|
-
this.notify();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
108
|
get(runId: string): RunHandle | undefined {
|
|
122
|
-
|
|
109
|
+
const handle = this.runs.get(runId);
|
|
110
|
+
return handle ? publicHandle(handle) : undefined;
|
|
123
111
|
}
|
|
124
112
|
|
|
125
113
|
list(): RunHandle[] {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.map((id) => scope.runs.get(id))
|
|
114
|
+
return this.order
|
|
115
|
+
.map((id) => this.runs.get(id))
|
|
129
116
|
.filter((handle): handle is RunHandle => Boolean(handle))
|
|
117
|
+
.map(publicHandle)
|
|
130
118
|
.sort((left, right) => right.startedAt - left.startedAt);
|
|
131
119
|
}
|
|
132
120
|
|
|
133
121
|
active(): RunHandle[] {
|
|
134
|
-
return this.list().filter((
|
|
122
|
+
return this.list().filter((handle) => handle.snapshot.status === "running");
|
|
135
123
|
}
|
|
136
124
|
|
|
137
125
|
abortAll(): void {
|
|
138
|
-
for (const handle of this.
|
|
139
|
-
if (handle.snapshot.status
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
126
|
+
for (const handle of this.runs.values()) {
|
|
127
|
+
if (handle.snapshot.status !== "running") continue;
|
|
128
|
+
try {
|
|
129
|
+
handle.abort();
|
|
130
|
+
} catch {
|
|
131
|
+
// A failing callback must not prevent cancellation of sibling runs.
|
|
145
132
|
}
|
|
146
133
|
}
|
|
147
134
|
}
|
|
148
135
|
|
|
149
|
-
private
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (!scope) {
|
|
153
|
-
scope = { runs: new Map(), order: [] };
|
|
154
|
-
this.scopes.set(key, scope);
|
|
155
|
-
}
|
|
156
|
-
return scope;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private trimRuns(scope: RegistryScope): void {
|
|
160
|
-
while (scope.runs.size > 50) {
|
|
161
|
-
const candidate = [...scope.runs.entries()].sort((left, right) => {
|
|
136
|
+
private trimRuns(): void {
|
|
137
|
+
while (this.runs.size > 50) {
|
|
138
|
+
const candidate = [...this.runs.entries()].sort((left, right) => {
|
|
162
139
|
const leftActive = left[1].snapshot.status === "running" ? 1 : 0;
|
|
163
140
|
const rightActive = right[1].snapshot.status === "running" ? 1 : 0;
|
|
164
141
|
return leftActive - rightActive || left[1].startedAt - right[1].startedAt;
|
|
165
142
|
})[0];
|
|
166
143
|
if (!candidate) break;
|
|
167
|
-
|
|
168
|
-
|
|
144
|
+
this.runs.delete(candidate[0]);
|
|
145
|
+
this.order = this.order.filter((id) => id !== candidate[0]);
|
|
169
146
|
}
|
|
170
147
|
}
|
|
171
148
|
}
|
|
172
149
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
150
|
+
function publicHandle(handle: RunHandle): RunHandle {
|
|
151
|
+
return {
|
|
152
|
+
...handle,
|
|
153
|
+
snapshot: structuredClone(handle.snapshot),
|
|
154
|
+
};
|
|
178
155
|
}
|