@coreplane/switchboard 1.261.4 → 1.262.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/dist/assets/deploy/cloudflare/coordinator.ts +18 -1
- package/dist/assets/deploy/cloudflare/worker.ts +4 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +50 -2
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/coordinator/contract.ts +137 -4
- package/dist/assets/src/core/coordinator/driver.ts +192 -40
- package/dist/assets/src/core/refusal.ts +1 -0
- package/dist/assets/src/core/runEvents.ts +2 -0
- package/dist/assets/src/core/ship/coordinator.ts +91 -15
- package/dist/cli.js +786 -41
- package/package.json +1 -1
|
@@ -22,7 +22,9 @@ import { NonRetryableError } from "cloudflare:workflows";
|
|
|
22
22
|
import { COORDINATOR_IDENTITY, COORDINATOR_STEP_PATH_PREFIX } from "../../src/core/coordinator/contract.ts";
|
|
23
23
|
import {
|
|
24
24
|
readBotAnswer,
|
|
25
|
+
runOriginalUnitRecovery,
|
|
25
26
|
runPlan,
|
|
27
|
+
type OriginalUnitRecoveryParams,
|
|
26
28
|
type CoordinatorBot,
|
|
27
29
|
type PlanRunSummary,
|
|
28
30
|
type StepRunner,
|
|
@@ -38,7 +40,17 @@ export type CoordinatorEnv = Pick<Env, "SWITCHBOARD" | "SWITCHBOARD_INGRESS_TOKE
|
|
|
38
40
|
/** What an instance is created with: nothing the driver reads — the instance
|
|
39
41
|
* id names the plan, and the bot's rows are the input (the coordinator's
|
|
40
42
|
* contract: ids only, never a task's text or a thread's contents). */
|
|
41
|
-
export type ShipCoordinatorParams = Record<string, unknown
|
|
43
|
+
export type ShipCoordinatorParams = Record<string, unknown> | OriginalUnitRecoveryParams;
|
|
44
|
+
|
|
45
|
+
function originalUnitRecoveryParams(value: ShipCoordinatorParams): OriginalUnitRecoveryParams | undefined {
|
|
46
|
+
if (
|
|
47
|
+
value.kind !== "recover-original-unit" ||
|
|
48
|
+
typeof value.parentInstanceId !== "string" ||
|
|
49
|
+
typeof value.unit !== "string"
|
|
50
|
+
)
|
|
51
|
+
return undefined;
|
|
52
|
+
return { kind: value.kind, parentInstanceId: value.parentInstanceId, unit: value.unit };
|
|
53
|
+
}
|
|
42
54
|
|
|
43
55
|
/** The bot behind the container binding: the reply as the wire carried it.
|
|
44
56
|
* The transport's failures throw for the step's retry; the door's own refusal
|
|
@@ -81,6 +93,11 @@ function workflowSteps(step: WorkflowStep): StepRunner {
|
|
|
81
93
|
|
|
82
94
|
export class ShipCoordinator extends WorkflowEntrypoint<CoordinatorEnv, ShipCoordinatorParams> {
|
|
83
95
|
async run(event: Readonly<WorkflowEvent<ShipCoordinatorParams>>, step: WorkflowStep): Promise<PlanRunSummary> {
|
|
96
|
+
if (event.payload.kind === "recover-original-unit") {
|
|
97
|
+
const params = originalUnitRecoveryParams(event.payload);
|
|
98
|
+
if (params === undefined) throw new NonRetryableError("the original-unit recovery params are malformed");
|
|
99
|
+
return runOriginalUnitRecovery(workflowSteps(step), containerBot(this.env), event.instanceId, params);
|
|
100
|
+
}
|
|
84
101
|
return runPlan(workflowSteps(step), containerBot(this.env), event.instanceId);
|
|
85
102
|
}
|
|
86
103
|
}
|
|
@@ -325,7 +325,10 @@ async function handleCoordinatorInstances(request: Request, env: Env): Promise<R
|
|
|
325
325
|
const existing = await (await env.SHIP_COORDINATOR.get(parsed.id)).status();
|
|
326
326
|
outcome = { kind: "duplicate", id: parsed.id, status: existing.status };
|
|
327
327
|
} catch {
|
|
328
|
-
|
|
328
|
+
// Creation may have committed before its response was lost. When the
|
|
329
|
+
// status read is also unavailable, do not call that a definite failure:
|
|
330
|
+
// the bot must retain its same-id claim for a later duplicate replay.
|
|
331
|
+
return json(503, { ok: false, error: "create_unanswered", message: reason });
|
|
329
332
|
}
|
|
330
333
|
}
|
|
331
334
|
console.log(`[coordinator] ${auth.subject} → instance ${parsed.id}: ${outcome.kind}`);
|
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
sendRunFinished,
|
|
118
118
|
STEP_NAME_PATTERN,
|
|
119
119
|
UNIT_PATTERN,
|
|
120
|
+
unitOfIdempotencyKey,
|
|
120
121
|
type CoordinatorInstance,
|
|
121
122
|
type CoordinatorUnit,
|
|
122
123
|
type RunFinishedSend,
|
|
@@ -2771,6 +2772,37 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2771
2772
|
.map((r) => JSON.parse(r.json) as CoordinatorUnit);
|
|
2772
2773
|
}
|
|
2773
2774
|
|
|
2775
|
+
async listActiveRecoveries(): Promise<CoordinatorUnit[]> {
|
|
2776
|
+
return this.sql
|
|
2777
|
+
.exec<{ json: string }>(
|
|
2778
|
+
`SELECT json FROM coordinator_units
|
|
2779
|
+
WHERE json_valid(json) = 0 OR json_type(json, '$.recovery') IS NOT NULL
|
|
2780
|
+
ORDER BY rowid`,
|
|
2781
|
+
)
|
|
2782
|
+
.toArray()
|
|
2783
|
+
.map((r) => {
|
|
2784
|
+
const unit: unknown = JSON.parse(r.json);
|
|
2785
|
+
if (!isCoordinatorUnit(unit) || unit.recovery === undefined)
|
|
2786
|
+
throw new Error("active recovery index contains a malformed coordinator unit");
|
|
2787
|
+
return unit;
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
private recoveryTransport(parentInstanceId: string, idempotencyKey: string | undefined): string | undefined {
|
|
2792
|
+
const unit = idempotencyKey === undefined ? undefined : unitOfIdempotencyKey(idempotencyKey);
|
|
2793
|
+
if (unit === undefined) return undefined;
|
|
2794
|
+
const row = this.sql
|
|
2795
|
+
.exec<{ json: string }>(
|
|
2796
|
+
`SELECT json FROM coordinator_units WHERE instance_id = ? AND unit = ?`,
|
|
2797
|
+
parentInstanceId,
|
|
2798
|
+
unit,
|
|
2799
|
+
)
|
|
2800
|
+
.toArray()[0];
|
|
2801
|
+
if (row === undefined) return undefined;
|
|
2802
|
+
const parsed = JSON.parse(row.json) as CoordinatorUnit;
|
|
2803
|
+
return parsed.recovery?.workflowId;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2774
2806
|
// ---- the thread events of a unit-owned thread (record 0051's reply-as-event rule) --------------
|
|
2775
2807
|
|
|
2776
2808
|
/** The next sequence assigned in one transaction, the per-event cap applied
|
|
@@ -3025,9 +3057,11 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
3025
3057
|
// tells the waiting parent the child resumed — best effort, beside the
|
|
3026
3058
|
// bot's own announcement; a duplicate is consumed and re-armed, harmless.
|
|
3027
3059
|
if (req.meta.restartOf !== undefined && req.meta.parentInstanceId !== undefined) {
|
|
3060
|
+
const recoveryTransport = this.recoveryTransport(req.meta.parentInstanceId, req.meta.idempotencyKey);
|
|
3028
3061
|
const sent = await sendChildSignal(this.env.SHIP_COORDINATOR, {
|
|
3029
3062
|
runId: req.runId,
|
|
3030
3063
|
parentInstanceId: req.meta.parentInstanceId,
|
|
3064
|
+
...(recoveryTransport !== undefined ? { transportWorkflowId: recoveryTransport } : {}),
|
|
3031
3065
|
kind: "resumed",
|
|
3032
3066
|
reason: `restarted from run ${req.meta.restartOf}`,
|
|
3033
3067
|
at: now,
|
|
@@ -3318,7 +3352,14 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
3318
3352
|
if ((await this.ctx.storage.getAlarm()) === null)
|
|
3319
3353
|
await this.ctx.storage.setAlarm(systemClock() + RUN_SWEEP_INTERVAL_MS);
|
|
3320
3354
|
await this.refreshSessionBytes(record.session?.key);
|
|
3321
|
-
const
|
|
3355
|
+
const transportWorkflowId =
|
|
3356
|
+
record.parentInstanceId === undefined
|
|
3357
|
+
? undefined
|
|
3358
|
+
: this.recoveryTransport(record.parentInstanceId, record.idempotencyKey);
|
|
3359
|
+
const event = await sendRunFinished(this.env.SHIP_COORDINATOR, {
|
|
3360
|
+
...record,
|
|
3361
|
+
...(transportWorkflowId !== undefined ? { transportWorkflowId } : {}),
|
|
3362
|
+
});
|
|
3322
3363
|
if (event.kind === "failed")
|
|
3323
3364
|
console.warn(`[runs/finish] ${runId} → ${event.type} not delivered to ${event.instance}: ${event.reason}`);
|
|
3324
3365
|
return { ...out, event: event.kind };
|
|
@@ -3654,7 +3695,11 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
3654
3695
|
// `finishedAt` equals `startedAt`); the parent confirms by `read-record`
|
|
3655
3696
|
// before it acts, so a duplicate send is harmless.
|
|
3656
3697
|
if (result.stored && record.parentInstanceId !== undefined && record.finishedAt > record.startedAt) {
|
|
3657
|
-
const
|
|
3698
|
+
const transportWorkflowId = record.events.find((event) => event.type === "coordinator_tag")?.transportWorkflowId;
|
|
3699
|
+
const event = await sendRunFinished(this.env.SHIP_COORDINATOR, {
|
|
3700
|
+
...record,
|
|
3701
|
+
...(transportWorkflowId !== undefined ? { transportWorkflowId } : {}),
|
|
3702
|
+
});
|
|
3658
3703
|
if (event.kind === "failed")
|
|
3659
3704
|
console.warn(`[runs/put] ${record.id} → ${event.type} not delivered to ${event.instance}: ${event.reason}`);
|
|
3660
3705
|
}
|
|
@@ -5220,6 +5265,7 @@ const LEDGER_ROUTES = new Set([
|
|
|
5220
5265
|
"/runs/coordinator/stop",
|
|
5221
5266
|
"/runs/coordinator/units/put",
|
|
5222
5267
|
"/runs/coordinator/units/claim-legacy-continuation",
|
|
5268
|
+
"/runs/coordinator/units/list-active-recoveries",
|
|
5223
5269
|
"/runs/coordinator/units/list",
|
|
5224
5270
|
"/runs/coordinator/events/append",
|
|
5225
5271
|
"/runs/coordinator/events/list",
|
|
@@ -5935,6 +5981,8 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
5935
5981
|
return json({ error: "instanceId must be a Workflow instance id" }, 400);
|
|
5936
5982
|
return json({ units: await stub.listUnits(b.instanceId) });
|
|
5937
5983
|
}
|
|
5984
|
+
if (pathname === "/runs/coordinator/units/list-active-recoveries")
|
|
5985
|
+
return json({ units: await stub.listActiveRecoveries() });
|
|
5938
5986
|
if (pathname === "/runs/coordinator/wake") {
|
|
5939
5987
|
if (!isCoordinatorUnit(b.unit)) return json({ error: "unit must be a coordinator unit row" }, 400);
|
|
5940
5988
|
if (typeof b.waitId !== "string" || !STEP_NAME_PATTERN.test(b.waitId))
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.262.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.262.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -21060,7 +21060,7 @@
|
|
|
21060
21060
|
},
|
|
21061
21061
|
"packages/switchboard": {
|
|
21062
21062
|
"name": "@coreplane/switchboard",
|
|
21063
|
-
"version": "1.
|
|
21063
|
+
"version": "1.262.0",
|
|
21064
21064
|
"license": "Apache-2.0",
|
|
21065
21065
|
"dependencies": {
|
|
21066
21066
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.262.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -313,6 +313,20 @@ export interface ExistingPrPublicationBinding {
|
|
|
313
313
|
export interface CoordinatorTag {
|
|
314
314
|
parentInstanceId: string;
|
|
315
315
|
idempotencyKey: string;
|
|
316
|
+
/** Workflow transport for a recovered child. Identity and idempotency remain
|
|
317
|
+
* on `parentInstanceId`; only lifecycle wake-ups use this checkpoint id. */
|
|
318
|
+
transportWorkflowId?: string;
|
|
319
|
+
/** The immutable target and absolute lease of an original-unit recovery.
|
|
320
|
+
* Stored on the coordinator event so a resume or request-restart cannot turn
|
|
321
|
+
* the remaining lease into a fresh relative budget or adopt another head. */
|
|
322
|
+
recovery?: {
|
|
323
|
+
repo: string;
|
|
324
|
+
pr: number;
|
|
325
|
+
headRef: string;
|
|
326
|
+
baseRef: string;
|
|
327
|
+
expectedHeadSha: string;
|
|
328
|
+
deadlineAt: number;
|
|
329
|
+
};
|
|
316
330
|
/** The branch the child's pull request targets: the plan's base
|
|
317
331
|
* (`CoordinatorInstance.base`), set by the spawn when the instance knows it.
|
|
318
332
|
* A coordinator's child is dispatched AT its unit branch so the resident
|
|
@@ -477,6 +491,45 @@ export interface RoundGate {
|
|
|
477
491
|
findings: string[];
|
|
478
492
|
}
|
|
479
493
|
|
|
494
|
+
/** One admitted continuation of an ended original unit. The claim replaces
|
|
495
|
+
* the terminal interpretation before a child starts; its step remains under
|
|
496
|
+
* the original instance/unit idempotency namespace. */
|
|
497
|
+
export interface OriginalUnitRecovery {
|
|
498
|
+
kind: "findings" | "review";
|
|
499
|
+
round: number;
|
|
500
|
+
expectedHeadSha: string;
|
|
501
|
+
remainingMs: number;
|
|
502
|
+
claimedAt: number;
|
|
503
|
+
step: string;
|
|
504
|
+
/** The original review record that authorizes this transition. */
|
|
505
|
+
reviewRunId: string;
|
|
506
|
+
/** A completed findings child that already advanced the original unit before
|
|
507
|
+
* recovery was claimed. Present only when recovery starts at re-review. */
|
|
508
|
+
findingsRunId?: string;
|
|
509
|
+
/** Exact durable key of that completed original findings child. */
|
|
510
|
+
findingsKey?: string;
|
|
511
|
+
/** The posted request-changes findings, retained so a Workflow restart can
|
|
512
|
+
* rebuild the exact findings state without trusting a later listing. */
|
|
513
|
+
findings?: import("../reviewVerdict.js").Finding[];
|
|
514
|
+
/** The exact terminal value replaced by the claim, for fail-closed rollback
|
|
515
|
+
* if the Workflow cannot be admitted. */
|
|
516
|
+
previousEnding: NonNullable<CoordinatorUnit["ending"]>;
|
|
517
|
+
/** The separate Workflow execution checkpoint. This is transport identity,
|
|
518
|
+
* not a replacement coordinator/unit identity. */
|
|
519
|
+
workflowId: string;
|
|
520
|
+
/** Absolute end of the original active lease. Delayed Workflow admission or
|
|
521
|
+
* replay cannot turn the claim's snapshot into fresh time. */
|
|
522
|
+
deadlineAt: number;
|
|
523
|
+
/** Exact durable child key of the review outcome that authorized recovery. */
|
|
524
|
+
reviewKey: string;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface OriginalUnitRecoveryReceipt {
|
|
528
|
+
reviewRunId: string;
|
|
529
|
+
workflowId: string;
|
|
530
|
+
at: number;
|
|
531
|
+
}
|
|
532
|
+
|
|
480
533
|
/** One unit of the plan an instance runs (a task string is a generated plan of
|
|
481
534
|
* one unit, `U1`): its branch, the units it waits on, and — as the runner
|
|
482
535
|
* reaches it — its thread, its pull request, the round boundaries the card
|
|
@@ -543,6 +596,15 @@ export interface CoordinatorUnit {
|
|
|
543
596
|
* item 9) — a mismatch to be seen, since the child's parser holds an approve
|
|
544
597
|
* to the same level. */
|
|
545
598
|
rounds: Array<{ index: number; agent: string; outcome: string; at: number; gate?: RoundGate }>;
|
|
599
|
+
/** An explicit recovery claim for this same durable unit. It is mutually
|
|
600
|
+
* exclusive with both `idle` and `ending`; legacy readers otherwise keep
|
|
601
|
+
* their existing decoding rules. */
|
|
602
|
+
recovery?: OriginalUnitRecovery;
|
|
603
|
+
/** The authorizing review already consumed by a completed recovery. */
|
|
604
|
+
recoveryReceipt?: OriginalUnitRecoveryReceipt;
|
|
605
|
+
/** A recovered review that reached a person-only question. Recovery settles
|
|
606
|
+
* truthfully instead of opening an idle renewal or replacement pipeline. */
|
|
607
|
+
recoveryHold?: { cause: "human"; gate: HumanGatePending } | { cause: "draft"; pr: { number: number; url: string } };
|
|
546
608
|
/** How the unit ended: the ending's kind and the thread's report, when it
|
|
547
609
|
* has. `cause` names the machine's reason behind a driver-posted kind;
|
|
548
610
|
* `step` and `round` locate that reason without parsing the report. For a
|
|
@@ -727,6 +789,64 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
|
|
|
727
789
|
)
|
|
728
790
|
)
|
|
729
791
|
return false;
|
|
792
|
+
if (
|
|
793
|
+
r.recovery !== undefined &&
|
|
794
|
+
!(
|
|
795
|
+
isObject(r.recovery) &&
|
|
796
|
+
(r.recovery.kind === "findings" || r.recovery.kind === "review") &&
|
|
797
|
+
typeof r.recovery.round === "number" &&
|
|
798
|
+
Number.isInteger(r.recovery.round) &&
|
|
799
|
+
r.recovery.round >= 1 &&
|
|
800
|
+
typeof r.recovery.expectedHeadSha === "string" &&
|
|
801
|
+
/^[0-9a-f]{40}$/i.test(r.recovery.expectedHeadSha) &&
|
|
802
|
+
isFinite(r.recovery.remainingMs) &&
|
|
803
|
+
r.recovery.remainingMs > 0 &&
|
|
804
|
+
isFinite(r.recovery.claimedAt) &&
|
|
805
|
+
typeof r.recovery.step === "string" &&
|
|
806
|
+
STEP_NAME_PATTERN.test(r.recovery.step) &&
|
|
807
|
+
isText(r.recovery.reviewRunId) &&
|
|
808
|
+
(r.recovery.findingsRunId === undefined || isText(r.recovery.findingsRunId)) &&
|
|
809
|
+
(r.recovery.findingsKey === undefined || isText(r.recovery.findingsKey)) &&
|
|
810
|
+
((r.recovery.findingsRunId === undefined && r.recovery.findingsKey === undefined) ||
|
|
811
|
+
(r.recovery.kind === "review" &&
|
|
812
|
+
r.recovery.findingsRunId !== undefined &&
|
|
813
|
+
r.recovery.findingsKey !== undefined)) &&
|
|
814
|
+
(r.recovery.findings === undefined ||
|
|
815
|
+
(Array.isArray(r.recovery.findings) && r.recovery.findings.every(isFindingShape))) &&
|
|
816
|
+
isObject(r.recovery.previousEnding) &&
|
|
817
|
+
isText(r.recovery.previousEnding.kind) &&
|
|
818
|
+
typeof r.recovery.previousEnding.report === "string" &&
|
|
819
|
+
isFinite(r.recovery.previousEnding.at) &&
|
|
820
|
+
typeof r.recovery.workflowId === "string" &&
|
|
821
|
+
INSTANCE_ID_PATTERN.test(r.recovery.workflowId) &&
|
|
822
|
+
isFinite(r.recovery.deadlineAt) &&
|
|
823
|
+
isText(r.recovery.reviewKey)
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
return false;
|
|
827
|
+
if (
|
|
828
|
+
r.recoveryReceipt !== undefined &&
|
|
829
|
+
(!isObject(r.recoveryReceipt) ||
|
|
830
|
+
!isText(r.recoveryReceipt.reviewRunId) ||
|
|
831
|
+
typeof r.recoveryReceipt.workflowId !== "string" ||
|
|
832
|
+
!INSTANCE_ID_PATTERN.test(r.recoveryReceipt.workflowId) ||
|
|
833
|
+
!isFinite(r.recoveryReceipt.at))
|
|
834
|
+
)
|
|
835
|
+
return false;
|
|
836
|
+
if (
|
|
837
|
+
r.recoveryHold !== undefined &&
|
|
838
|
+
(!isObject(r.recoveryHold) ||
|
|
839
|
+
!(
|
|
840
|
+
(r.recoveryHold.cause === "human" && isHumanGatePending(r.recoveryHold.gate)) ||
|
|
841
|
+
(r.recoveryHold.cause === "draft" &&
|
|
842
|
+
isObject(r.recoveryHold.pr) &&
|
|
843
|
+
Number.isInteger(r.recoveryHold.pr.number) &&
|
|
844
|
+
(r.recoveryHold.pr.number as number) > 0 &&
|
|
845
|
+
isText(r.recoveryHold.pr.url))
|
|
846
|
+
))
|
|
847
|
+
)
|
|
848
|
+
return false;
|
|
849
|
+
if (r.recovery !== undefined && (r.idle !== undefined || r.ending !== undefined)) return false;
|
|
730
850
|
if (
|
|
731
851
|
r.ending !== undefined &&
|
|
732
852
|
!(
|
|
@@ -829,13 +949,20 @@ export async function sendPullMerged(
|
|
|
829
949
|
* send costs the wait a chunk, never the round. */
|
|
830
950
|
export async function sendChildSignal(
|
|
831
951
|
workflow: WorkflowSender | undefined,
|
|
832
|
-
signal: {
|
|
952
|
+
signal: {
|
|
953
|
+
runId: string;
|
|
954
|
+
parentInstanceId: string;
|
|
955
|
+
transportWorkflowId?: string;
|
|
956
|
+
kind: "interrupted" | "resumed";
|
|
957
|
+
reason: string;
|
|
958
|
+
at: number;
|
|
959
|
+
},
|
|
833
960
|
): Promise<RunFinishedSend> {
|
|
834
961
|
const { runId, parentInstanceId: instance, kind, reason, at } = signal;
|
|
835
962
|
if (!workflow) return { kind: "no-binding", instance };
|
|
836
963
|
const type = kind === "interrupted" ? childInterruptedEventType(runId) : childResumedEventType(runId);
|
|
837
964
|
try {
|
|
838
|
-
const handle = await workflow.get(instance);
|
|
965
|
+
const handle = await workflow.get(signal.transportWorkflowId ?? instance);
|
|
839
966
|
await handle.sendEvent({ type, payload: { runId, kind, reason, at, parentInstanceId: instance } });
|
|
840
967
|
return { kind: "sent", instance, type };
|
|
841
968
|
} catch (err) {
|
|
@@ -846,7 +973,13 @@ export async function sendChildSignal(
|
|
|
846
973
|
/** The one send per committed terminal record (run-history item 47). */
|
|
847
974
|
export async function sendRunFinished(
|
|
848
975
|
workflow: WorkflowSender | undefined,
|
|
849
|
-
record: {
|
|
976
|
+
record: {
|
|
977
|
+
id: string;
|
|
978
|
+
status: string;
|
|
979
|
+
finishedAt: number;
|
|
980
|
+
parentInstanceId?: string;
|
|
981
|
+
transportWorkflowId?: string;
|
|
982
|
+
},
|
|
850
983
|
): Promise<RunFinishedSend> {
|
|
851
984
|
const instance = record.parentInstanceId;
|
|
852
985
|
if (instance === undefined) return { kind: "none" };
|
|
@@ -859,7 +992,7 @@ export async function sendRunFinished(
|
|
|
859
992
|
parentInstanceId: instance,
|
|
860
993
|
};
|
|
861
994
|
try {
|
|
862
|
-
const handle = await workflow.get(instance);
|
|
995
|
+
const handle = await workflow.get(record.transportWorkflowId ?? instance);
|
|
863
996
|
await handle.sendEvent({ type, payload });
|
|
864
997
|
return { kind: "sent", instance, type };
|
|
865
998
|
} catch (err) {
|