|
@@ -120,6 +120,13 @@ type SyntheticTerminalOccurrenceLookup =
|
|
|
reason: string;
|
|
reason: string;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+type SyntheticTerminalProvenanceKind =
|
|
|
|
|
+ | 'explicit'
|
|
|
|
|
+ | 'host-message'
|
|
|
|
|
+ | 'legacy';
|
|
|
|
|
+
|
|
|
|
|
+const HOST_MESSAGE_OCCURRENCE_PREFIX = 'host-message:';
|
|
|
|
|
+
|
|
|
export interface InjectionState {
|
|
export interface InjectionState {
|
|
|
backgroundJobBoard: BackgroundJobStore;
|
|
backgroundJobBoard: BackgroundJobStore;
|
|
|
lifecycleLedger: BackgroundJobLifecycleLedger;
|
|
lifecycleLedger: BackgroundJobLifecycleLedger;
|
|
@@ -217,26 +224,47 @@ function createOccurrenceId(
|
|
|
return explicitOccurrenceID;
|
|
return explicitOccurrenceID;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (typeof message.info.id === 'string') {
|
|
|
|
|
- return `${message.info.id}:${partIndex}`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
const sessionID = message.info.sessionID ?? 'unknown';
|
|
const sessionID = message.info.sessionID ?? 'unknown';
|
|
|
const content = typeof part.text === 'string' ? part.text : '';
|
|
const content = typeof part.text === 'string' ? part.text : '';
|
|
|
|
|
|
|
|
const status = parseTaskStatusOutput(content);
|
|
const status = parseTaskStatusOutput(content);
|
|
|
if (status) {
|
|
if (status) {
|
|
|
|
|
+ const messageID = getCanonicalHostMessageID(part, message);
|
|
|
|
|
+ if (messageID) {
|
|
|
|
|
+ return hostMessageOccurrenceID(messageID, status.taskID, content);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof message.info.id === 'string') {
|
|
|
|
|
+ return `${message.info.id}:${partIndex}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const stableKey = `${sessionID}:${status.taskID}:${status.state}:${status.result ?? ''}`;
|
|
const stableKey = `${sessionID}:${status.taskID}:${status.state}:${status.result ?? ''}`;
|
|
|
const hash = djb2Hash(stableKey);
|
|
const hash = djb2Hash(stableKey);
|
|
|
return `anon:${hash}`;
|
|
return `anon:${hash}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (typeof message.info.id === 'string') {
|
|
|
|
|
+ return `${message.info.id}:${partIndex}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const hash = djb2Hash(`${sessionID}:${content}`);
|
|
const hash = djb2Hash(`${sessionID}:${content}`);
|
|
|
return `anon:${hash}`;
|
|
return `anon:${hash}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function injectedCompletionKey(taskID: string, occurrenceId: string): string {
|
|
|
|
|
- return `${taskID}\u001f${occurrenceId}`;
|
|
|
|
|
|
|
+function injectedCompletionKey(
|
|
|
|
|
+ taskID: string,
|
|
|
|
|
+ occurrenceId: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
|
|
+): string {
|
|
|
|
|
+ return `${provenanceKind}\u001f${taskID}\u001f${occurrenceId}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function hasProvenanceKind(
|
|
|
|
|
+ key: string,
|
|
|
|
|
+ taskID: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
|
|
+): boolean {
|
|
|
|
|
+ return key.startsWith(`${provenanceKind}\u001f${taskID}\u001f`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getExplicitOccurrenceID(part: MessagePart): string | undefined {
|
|
function getExplicitOccurrenceID(part: MessagePart): string | undefined {
|
|
@@ -247,6 +275,42 @@ function getExplicitOccurrenceID(part: MessagePart): string | undefined {
|
|
|
return undefined;
|
|
return undefined;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function getHostMessageID(part: MessagePart): string | undefined {
|
|
|
|
|
+ return typeof part.messageID === 'string' && part.messageID.trim() !== ''
|
|
|
|
|
+ ? part.messageID
|
|
|
|
|
+ : undefined;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getCanonicalHostMessageID(
|
|
|
|
|
+ part: MessagePart,
|
|
|
|
|
+ message?: MessageWithParts,
|
|
|
|
|
+): string | undefined {
|
|
|
|
|
+ return (
|
|
|
|
|
+ getHostMessageID(part) ??
|
|
|
|
|
+ (typeof message?.info.id === 'string' && message.info.id.trim() !== ''
|
|
|
|
|
+ ? message.info.id
|
|
|
|
|
+ : undefined)
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function hostMessageOccurrenceID(
|
|
|
|
|
+ messageID: string,
|
|
|
|
|
+ taskID: string,
|
|
|
|
|
+ terminalPayload: string,
|
|
|
|
|
+): string {
|
|
|
|
|
+ return `${HOST_MESSAGE_OCCURRENCE_PREFIX}${djb2Hash(
|
|
|
|
|
+ `${messageID}:${taskID}`,
|
|
|
|
|
+ )}:${sha256Hash(terminalPayload)}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function provenanceKindForPart(
|
|
|
|
|
+ part: MessagePart,
|
|
|
|
|
+ message?: MessageWithParts,
|
|
|
|
|
+): SyntheticTerminalProvenanceKind {
|
|
|
|
|
+ if (getExplicitOccurrenceID(part)) return 'explicit';
|
|
|
|
|
+ return getCanonicalHostMessageID(part, message) ? 'host-message' : 'legacy';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function isProcessableSyntheticTerminal(
|
|
function isProcessableSyntheticTerminal(
|
|
|
text: string,
|
|
text: string,
|
|
|
status: TaskStatusOutput,
|
|
status: TaskStatusOutput,
|
|
@@ -276,11 +340,21 @@ function observationOccurrenceID(
|
|
|
return { occurrenceID: explicitOccurrenceID, reliable: true };
|
|
return { occurrenceID: explicitOccurrenceID, reliable: true };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const messageID =
|
|
|
|
|
- typeof part.messageID === 'string' ? part.messageID : 'unknown-message';
|
|
|
|
|
|
|
+ const messageID = getHostMessageID(part);
|
|
|
|
|
+ if (messageID) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ occurrenceID: hostMessageOccurrenceID(
|
|
|
|
|
+ messageID,
|
|
|
|
|
+ status.taskID,
|
|
|
|
|
+ part.text ?? '',
|
|
|
|
|
+ ),
|
|
|
|
|
+ reliable: true,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
occurrenceID: `ambiguous:${djb2Hash(
|
|
occurrenceID: `ambiguous:${djb2Hash(
|
|
|
- `${messageID}:${status.taskID}:${part.text ?? ''}`,
|
|
|
|
|
|
|
+ `unknown-message:${status.taskID}:${part.text ?? ''}`,
|
|
|
)}`,
|
|
)}`,
|
|
|
reliable: false,
|
|
reliable: false,
|
|
|
};
|
|
};
|
|
@@ -289,12 +363,17 @@ function observationOccurrenceID(
|
|
|
function rememberSyntheticTerminalOccurrence(
|
|
function rememberSyntheticTerminalOccurrence(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
origin: SyntheticTerminalOccurrenceOrigin,
|
|
origin: SyntheticTerminalOccurrenceOrigin,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
): void {
|
|
): void {
|
|
|
const occurrences = state.syntheticTerminalOccurrences;
|
|
const occurrences = state.syntheticTerminalOccurrences;
|
|
|
const order = state.syntheticTerminalOccurrenceOrder;
|
|
const order = state.syntheticTerminalOccurrenceOrder;
|
|
|
if (!occurrences || !order) return;
|
|
if (!occurrences || !order) return;
|
|
|
|
|
|
|
|
- const key = injectedCompletionKey(origin.taskID, origin.occurrenceID);
|
|
|
|
|
|
|
+ const key = injectedCompletionKey(
|
|
|
|
|
+ origin.taskID,
|
|
|
|
|
+ origin.occurrenceID,
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ );
|
|
|
const existing = occurrences.get(key);
|
|
const existing = occurrences.get(key);
|
|
|
if (existing) {
|
|
if (existing) {
|
|
|
// The first observation owns the lifecycle provenance. A later event for
|
|
// The first observation owns the lifecycle provenance. A later event for
|
|
@@ -315,28 +394,64 @@ function findSyntheticTerminalOccurrence(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
taskID: string,
|
|
taskID: string,
|
|
|
occurrenceID: string,
|
|
occurrenceID: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
|
|
+ explicitOccurrenceID: boolean,
|
|
|
|
|
+ currentGeneration: number | undefined,
|
|
|
|
|
+ currentTaskGeneration: number | undefined,
|
|
|
|
|
+ deletionEpoch: number | undefined,
|
|
|
): SyntheticTerminalOccurrenceLookup | undefined {
|
|
): SyntheticTerminalOccurrenceLookup | undefined {
|
|
|
const occurrences = state.syntheticTerminalOccurrences;
|
|
const occurrences = state.syntheticTerminalOccurrences;
|
|
|
if (!occurrences) return undefined;
|
|
if (!occurrences) return undefined;
|
|
|
|
|
|
|
|
- const direct = occurrences.get(injectedCompletionKey(taskID, occurrenceID));
|
|
|
|
|
- if (direct) return { kind: 'matched', origin: direct };
|
|
|
|
|
-
|
|
|
|
|
- // Older/runtime-derived parts may lack an id in the transform payload even
|
|
|
|
|
- // though the event hook saw the same terminal text. Only use an ambiguous
|
|
|
|
|
- // fallback when there is exactly one candidate for this task. Multiple
|
|
|
|
|
- // candidates cannot establish ownership and must remain fail-closed.
|
|
|
|
|
- const candidates = [...occurrences.values()].filter(
|
|
|
|
|
- (origin) =>
|
|
|
|
|
- origin.taskID === taskID && origin.occurrenceID.startsWith('ambiguous:'),
|
|
|
|
|
|
|
+ const direct = occurrences.get(
|
|
|
|
|
+ injectedCompletionKey(taskID, occurrenceID, provenanceKind),
|
|
|
);
|
|
);
|
|
|
- if (candidates.length === 1) {
|
|
|
|
|
- return { kind: 'matched', origin: candidates[0] };
|
|
|
|
|
|
|
+ if (explicitOccurrenceID) {
|
|
|
|
|
+ return direct ? { kind: 'matched', origin: direct } : undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (direct && provenanceKind !== 'host-message') {
|
|
|
|
|
+ return { kind: 'matched', origin: direct };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (provenanceKind === 'host-message') {
|
|
|
|
|
+ // Host message ids are weaker than part ids. Restrict them to the first
|
|
|
|
|
+ // generation: unrelated tasks do not advance this task-local run proof.
|
|
|
|
|
+ if (deletionEpoch !== undefined || currentTaskGeneration !== 1) {
|
|
|
|
|
+ return direct ? { kind: 'matched', origin: direct } : undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const candidates = [...occurrences.entries()]
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ ([key, origin]) =>
|
|
|
|
|
+ hasProvenanceKind(key, taskID, 'host-message') &&
|
|
|
|
|
+ origin.taskID === taskID &&
|
|
|
|
|
+ origin.generationAtObservation === currentGeneration,
|
|
|
|
|
+ )
|
|
|
|
|
+ .map(([, origin]) => origin);
|
|
|
|
|
+ if (candidates.length === 1 && candidates[0].occurrenceID === occurrenceID) {
|
|
|
|
|
+ return { kind: 'matched', origin: candidates[0] };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (candidates.length > 1) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ kind: 'ambiguous',
|
|
|
|
|
+ reason: `multiple current-generation host message.part.updated origins (${candidates.length}) could not be uniquely matched`,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return direct ? { kind: 'matched', origin: direct } : undefined;
|
|
|
}
|
|
}
|
|
|
- if (candidates.length > 1) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Older/runtime-derived parts may lack an id in the transform payload. Do
|
|
|
|
|
+ // not guess ownership from an ambiguous event; the legacy path is allowed to
|
|
|
|
|
+ // fail closed, but it may not manufacture a match from candidate count.
|
|
|
|
|
+ const legacyCandidates = [...occurrences.entries()].filter(
|
|
|
|
|
+ ([key, origin]) =>
|
|
|
|
|
+ hasProvenanceKind(key, taskID, 'legacy') && origin.taskID === taskID,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (legacyCandidates.length > 0) {
|
|
|
return {
|
|
return {
|
|
|
kind: 'ambiguous',
|
|
kind: 'ambiguous',
|
|
|
- reason: `multiple ambiguous message.part.updated origins (${candidates.length}) could not be uniquely matched`,
|
|
|
|
|
|
|
+ reason: 'legacy weak provenance did not match a canonical host identity',
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
return undefined;
|
|
return undefined;
|
|
@@ -346,6 +461,7 @@ function rememberProcessedSyntheticTerminal(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
taskID: string,
|
|
taskID: string,
|
|
|
occurrenceID: string,
|
|
occurrenceID: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
origin: SyntheticTerminalOccurrenceOrigin | undefined,
|
|
origin: SyntheticTerminalOccurrenceOrigin | undefined,
|
|
|
generation: number | undefined,
|
|
generation: number | undefined,
|
|
|
): void {
|
|
): void {
|
|
@@ -354,29 +470,38 @@ function rememberProcessedSyntheticTerminal(
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- rememberSyntheticTerminalOccurrence(state, {
|
|
|
|
|
- taskID,
|
|
|
|
|
- occurrenceID,
|
|
|
|
|
- generationAtObservation: generation,
|
|
|
|
|
- lifecycleEpochAtObservation: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
- phase: 'processed',
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ rememberSyntheticTerminalOccurrence(
|
|
|
|
|
+ state,
|
|
|
|
|
+ {
|
|
|
|
|
+ taskID,
|
|
|
|
|
+ occurrenceID,
|
|
|
|
|
+ generationAtObservation: generation,
|
|
|
|
|
+ lifecycleEpochAtObservation: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
+ phase: 'processed',
|
|
|
|
|
+ },
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function failClosedSyntheticTerminal(
|
|
function failClosedSyntheticTerminal(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
status: TaskStatusOutput,
|
|
status: TaskStatusOutput,
|
|
|
occurrenceID: string,
|
|
occurrenceID: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
existing: BackgroundJobRecord | undefined,
|
|
existing: BackgroundJobRecord | undefined,
|
|
|
reason: string,
|
|
reason: string,
|
|
|
): void {
|
|
): void {
|
|
|
- rememberSyntheticTerminalOccurrence(state, {
|
|
|
|
|
- taskID: status.taskID,
|
|
|
|
|
- occurrenceID,
|
|
|
|
|
- generationAtObservation: existing?.generation,
|
|
|
|
|
- lifecycleEpochAtObservation: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
- phase: 'ambiguous',
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ rememberSyntheticTerminalOccurrence(
|
|
|
|
|
+ state,
|
|
|
|
|
+ {
|
|
|
|
|
+ taskID: status.taskID,
|
|
|
|
|
+ occurrenceID,
|
|
|
|
|
+ generationAtObservation: existing?.generation,
|
|
|
|
|
+ lifecycleEpochAtObservation: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
+ phase: 'ambiguous',
|
|
|
|
|
+ },
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
if (existing?.state === 'running') {
|
|
if (existing?.state === 'running') {
|
|
|
markSyntheticTerminalUncertain(
|
|
markSyntheticTerminalUncertain(
|
|
@@ -427,10 +552,15 @@ export function observeSyntheticTerminalPart(
|
|
|
if (!status || !isProcessableSyntheticTerminal(part.text, status)) return;
|
|
if (!status || !isProcessableSyntheticTerminal(part.text, status)) return;
|
|
|
|
|
|
|
|
const { occurrenceID, reliable } = observationOccurrenceID(part, status);
|
|
const { occurrenceID, reliable } = observationOccurrenceID(part, status);
|
|
|
|
|
+ const provenanceKind = provenanceKindForPart(part);
|
|
|
const existing = state.backgroundJobBoard.get(status.taskID);
|
|
const existing = state.backgroundJobBoard.get(status.taskID);
|
|
|
const lifecycleEpoch = state.getLifecycleEpoch?.() ?? 0;
|
|
const lifecycleEpoch = state.getLifecycleEpoch?.() ?? 0;
|
|
|
const deletionEpoch = state.getDeletionEpoch?.(status.taskID);
|
|
const deletionEpoch = state.getDeletionEpoch?.(status.taskID);
|
|
|
- const occurrenceKey = injectedCompletionKey(status.taskID, occurrenceID);
|
|
|
|
|
|
|
+ const occurrenceKey = injectedCompletionKey(
|
|
|
|
|
+ status.taskID,
|
|
|
|
|
+ occurrenceID,
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ );
|
|
|
const priorOccurrence =
|
|
const priorOccurrence =
|
|
|
state.syntheticTerminalOccurrences?.get(occurrenceKey);
|
|
state.syntheticTerminalOccurrences?.get(occurrenceKey);
|
|
|
const hasPreDeletionProvenance =
|
|
const hasPreDeletionProvenance =
|
|
@@ -438,28 +568,38 @@ export function observeSyntheticTerminalPart(
|
|
|
(priorOccurrence !== undefined &&
|
|
(priorOccurrence !== undefined &&
|
|
|
priorOccurrence.lifecycleEpochAtObservation < deletionEpoch &&
|
|
priorOccurrence.lifecycleEpochAtObservation < deletionEpoch &&
|
|
|
priorOccurrence.phase !== 'ambiguous');
|
|
priorOccurrence.phase !== 'ambiguous');
|
|
|
|
|
+ const hostMessageProvenance = provenanceKind === 'host-message';
|
|
|
const phase: SyntheticTerminalOccurrencePhase =
|
|
const phase: SyntheticTerminalOccurrencePhase =
|
|
|
- !reliable || !existing || !hasPreDeletionProvenance
|
|
|
|
|
|
|
+ !reliable ||
|
|
|
|
|
+ !existing ||
|
|
|
|
|
+ (hostMessageProvenance
|
|
|
|
|
+ ? deletionEpoch !== undefined || existing.taskGeneration !== 1
|
|
|
|
|
+ : !hasPreDeletionProvenance)
|
|
|
? 'ambiguous'
|
|
? 'ambiguous'
|
|
|
: 'observed';
|
|
: 'observed';
|
|
|
|
|
|
|
|
- rememberSyntheticTerminalOccurrence(state, {
|
|
|
|
|
- taskID: status.taskID,
|
|
|
|
|
- occurrenceID,
|
|
|
|
|
- generationAtObservation: existing?.generation,
|
|
|
|
|
- lifecycleEpochAtObservation: lifecycleEpoch,
|
|
|
|
|
- phase,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ rememberSyntheticTerminalOccurrence(
|
|
|
|
|
+ state,
|
|
|
|
|
+ {
|
|
|
|
|
+ taskID: status.taskID,
|
|
|
|
|
+ occurrenceID,
|
|
|
|
|
+ generationAtObservation: existing?.generation,
|
|
|
|
|
+ lifecycleEpochAtObservation: lifecycleEpoch,
|
|
|
|
|
+ phase,
|
|
|
|
|
+ },
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function hasRememberedInjectedCompletion(
|
|
function hasRememberedInjectedCompletion(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
taskID: string,
|
|
taskID: string,
|
|
|
occurrenceId: string,
|
|
occurrenceId: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
currentGeneration: number | undefined,
|
|
currentGeneration: number | undefined,
|
|
|
): boolean {
|
|
): boolean {
|
|
|
const fence = state.injectedCompletionFences?.get(
|
|
const fence = state.injectedCompletionFences?.get(
|
|
|
- injectedCompletionKey(taskID, occurrenceId),
|
|
|
|
|
|
|
+ injectedCompletionKey(taskID, occurrenceId, provenanceKind),
|
|
|
);
|
|
);
|
|
|
if (!fence) return false;
|
|
if (!fence) return false;
|
|
|
|
|
|
|
@@ -487,14 +627,16 @@ function hasRememberedInjectedCompletion(
|
|
|
function rememberInjectedCompletionFence(
|
|
function rememberInjectedCompletionFence(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
occurrenceId: string,
|
|
occurrenceId: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
fence: InjectedCompletionFence,
|
|
fence: InjectedCompletionFence,
|
|
|
): void {
|
|
): void {
|
|
|
const fences = state.injectedCompletionFences;
|
|
const fences = state.injectedCompletionFences;
|
|
|
if (!fences) return;
|
|
if (!fences) return;
|
|
|
|
|
|
|
|
- fences.set(injectedCompletionKey(fence.taskID, occurrenceId), {
|
|
|
|
|
- ...fence,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ fences.set(
|
|
|
|
|
+ injectedCompletionKey(fence.taskID, occurrenceId, provenanceKind),
|
|
|
|
|
+ { ...fence },
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ── Exported functions ─────────────────────────────────────────────────
|
|
// ── Exported functions ─────────────────────────────────────────────────
|
|
@@ -588,22 +730,30 @@ export function updateFromInjectedCompletion(
|
|
|
if (summary && !isCompleted && !isFailed) return undefined;
|
|
if (summary && !isCompleted && !isFailed) return undefined;
|
|
|
|
|
|
|
|
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
|
|
|
+ const provenanceKind = provenanceKindForPart(part, message);
|
|
|
|
|
+ const hasExplicitOccurrenceID = provenanceKind === 'explicit';
|
|
|
|
|
|
|
|
const existing = state.backgroundJobBoard.get(status.taskID);
|
|
const existing = state.backgroundJobBoard.get(status.taskID);
|
|
|
|
|
+ const deletionEpoch = state.getDeletionEpoch?.(status.taskID);
|
|
|
const occurrenceLookup = findSyntheticTerminalOccurrence(
|
|
const occurrenceLookup = findSyntheticTerminalOccurrence(
|
|
|
state,
|
|
state,
|
|
|
status.taskID,
|
|
status.taskID,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ hasExplicitOccurrenceID,
|
|
|
|
|
+ existing?.generation,
|
|
|
|
|
+ existing?.taskGeneration,
|
|
|
|
|
+ deletionEpoch,
|
|
|
);
|
|
);
|
|
|
const origin =
|
|
const origin =
|
|
|
occurrenceLookup?.kind === 'matched' ? occurrenceLookup.origin : undefined;
|
|
occurrenceLookup?.kind === 'matched' ? occurrenceLookup.origin : undefined;
|
|
|
- const deletionEpoch = state.getDeletionEpoch?.(status.taskID);
|
|
|
|
|
|
|
|
|
|
if (occurrenceLookup?.kind === 'ambiguous') {
|
|
if (occurrenceLookup?.kind === 'ambiguous') {
|
|
|
failClosedSyntheticTerminal(
|
|
failClosedSyntheticTerminal(
|
|
|
state,
|
|
state,
|
|
|
status,
|
|
status,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
existing,
|
|
existing,
|
|
|
occurrenceLookup.reason,
|
|
occurrenceLookup.reason,
|
|
|
);
|
|
);
|
|
@@ -617,12 +767,34 @@ export function updateFromInjectedCompletion(
|
|
|
state,
|
|
state,
|
|
|
status,
|
|
status,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
existing,
|
|
existing,
|
|
|
'message.part.updated origin was permanently ambiguous',
|
|
'message.part.updated origin was permanently ambiguous',
|
|
|
);
|
|
);
|
|
|
return undefined;
|
|
return undefined;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (provenanceKind === 'host-message') {
|
|
|
|
|
+ if (
|
|
|
|
|
+ !origin ||
|
|
|
|
|
+ !existing ||
|
|
|
|
|
+ deletionEpoch !== undefined ||
|
|
|
|
|
+ origin.phase !== 'observed' ||
|
|
|
|
|
+ existing.taskGeneration !== 1 ||
|
|
|
|
|
+ origin.generationAtObservation !== existing.generation
|
|
|
|
|
+ ) {
|
|
|
|
|
+ failClosedSyntheticTerminal(
|
|
|
|
|
+ state,
|
|
|
|
|
+ status,
|
|
|
|
|
+ occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ existing,
|
|
|
|
|
+ 'host messageID provenance did not identify exactly one current origin',
|
|
|
|
|
+ );
|
|
|
|
|
+ return undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (origin?.phase === 'observed') {
|
|
if (origin?.phase === 'observed') {
|
|
|
const generationChanged =
|
|
const generationChanged =
|
|
|
origin.generationAtObservation !== undefined &&
|
|
origin.generationAtObservation !== undefined &&
|
|
@@ -659,6 +831,7 @@ export function updateFromInjectedCompletion(
|
|
|
state,
|
|
state,
|
|
|
status,
|
|
status,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
existing,
|
|
existing,
|
|
|
'no message.part.updated origin was observed',
|
|
'no message.part.updated origin was observed',
|
|
|
);
|
|
);
|
|
@@ -670,6 +843,7 @@ export function updateFromInjectedCompletion(
|
|
|
state,
|
|
state,
|
|
|
status.taskID,
|
|
status.taskID,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
existing?.generation,
|
|
existing?.generation,
|
|
|
)
|
|
)
|
|
|
) {
|
|
) {
|
|
@@ -689,15 +863,22 @@ export function updateFromInjectedCompletion(
|
|
|
terminalState: existing?.terminalState,
|
|
terminalState: existing?.terminalState,
|
|
|
result: status.result,
|
|
result: status.result,
|
|
|
});
|
|
});
|
|
|
- rememberProcessedInjectedCompletion(state, status.taskID, occurrenceId, {
|
|
|
|
|
- taskID: status.taskID,
|
|
|
|
|
- generation: existing?.generation ?? 0,
|
|
|
|
|
- lifecycleEpoch: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ rememberProcessedInjectedCompletion(
|
|
|
|
|
+ state,
|
|
|
|
|
+ status.taskID,
|
|
|
|
|
+ occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ {
|
|
|
|
|
+ taskID: status.taskID,
|
|
|
|
|
+ generation: existing?.generation ?? 0,
|
|
|
|
|
+ lifecycleEpoch: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
rememberProcessedSyntheticTerminal(
|
|
rememberProcessedSyntheticTerminal(
|
|
|
state,
|
|
state,
|
|
|
status.taskID,
|
|
status.taskID,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
origin,
|
|
origin,
|
|
|
existing?.generation,
|
|
existing?.generation,
|
|
|
);
|
|
);
|
|
@@ -716,6 +897,7 @@ export function updateFromInjectedCompletion(
|
|
|
const processedOccurrenceKey = injectedCompletionKey(
|
|
const processedOccurrenceKey = injectedCompletionKey(
|
|
|
status.taskID,
|
|
status.taskID,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
);
|
|
);
|
|
|
if (state.processedInjectedCompletions.has(processedOccurrenceKey)) {
|
|
if (state.processedInjectedCompletions.has(processedOccurrenceKey)) {
|
|
|
return undefined;
|
|
return undefined;
|
|
@@ -747,14 +929,21 @@ export function updateFromInjectedCompletion(
|
|
|
state,
|
|
state,
|
|
|
updated.taskID,
|
|
updated.taskID,
|
|
|
occurrenceId,
|
|
occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
origin,
|
|
origin,
|
|
|
updated.generation,
|
|
updated.generation,
|
|
|
);
|
|
);
|
|
|
- rememberProcessedInjectedCompletion(state, status.taskID, occurrenceId, {
|
|
|
|
|
- taskID: updated.taskID,
|
|
|
|
|
- generation: updated.generation,
|
|
|
|
|
- lifecycleEpoch: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ rememberProcessedInjectedCompletion(
|
|
|
|
|
+ state,
|
|
|
|
|
+ status.taskID,
|
|
|
|
|
+ occurrenceId,
|
|
|
|
|
+ provenanceKind,
|
|
|
|
|
+ {
|
|
|
|
|
+ taskID: updated.taskID,
|
|
|
|
|
+ generation: updated.generation,
|
|
|
|
|
+ lifecycleEpoch: state.getLifecycleEpoch?.() ?? 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
return updated;
|
|
return updated;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -762,14 +951,15 @@ export function rememberProcessedInjectedCompletion(
|
|
|
state: InjectionState,
|
|
state: InjectionState,
|
|
|
taskID: string,
|
|
taskID: string,
|
|
|
occurrenceId: string,
|
|
occurrenceId: string,
|
|
|
|
|
+ provenanceKind: SyntheticTerminalProvenanceKind,
|
|
|
fence?: InjectedCompletionFence,
|
|
fence?: InjectedCompletionFence,
|
|
|
): void {
|
|
): void {
|
|
|
- const signature = injectedCompletionKey(taskID, occurrenceId);
|
|
|
|
|
|
|
+ const signature = injectedCompletionKey(taskID, occurrenceId, provenanceKind);
|
|
|
state.processedInjectedCompletions.add(signature);
|
|
state.processedInjectedCompletions.add(signature);
|
|
|
state.processedInjectedCompletionOrder.push(signature);
|
|
state.processedInjectedCompletionOrder.push(signature);
|
|
|
|
|
|
|
|
if (fence) {
|
|
if (fence) {
|
|
|
- rememberInjectedCompletionFence(state, occurrenceId, fence);
|
|
|
|
|
|
|
+ rememberInjectedCompletionFence(state, occurrenceId, provenanceKind, fence);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|