@piagent/platform 1.6.0 → 1.6.1
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 +49 -0
- package/README.md +17 -14
- package/README.vi.md +26 -1
- package/benchmarks/production-v1/spend-control.v1.json +27 -3
- package/benchmarks/production-v1/suite.json +5 -1
- package/catalog/capabilities.json +16 -16
- package/docs/authority-manifest.md +1 -1
- package/docs/auto-delegation-policy.md +38 -73
- package/docs/command-reference-vietnamese.md +23 -31
- package/docs/context-engine.md +5 -3
- package/docs/context-window-policy.md +37 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +26 -13
- package/docs/languages.json +4 -4
- package/docs/operator-manual-vietnamese.md +21 -41
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +179 -23
- package/docs/release-install-policy.md +6 -6
- package/docs/runtime-harness-standard.md +3 -3
- package/docs/security-threat-model.md +1 -1
- package/docs/subagent-orchestration-capabilities.md +19 -51
- package/docs/subagents-and-multiagent.md +21 -47
- package/docs/task-implementation-contract.md +3 -3
- package/docs/task-lifecycle-vietnamese.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +26 -13
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +89 -95
- package/docs-site/commands.html +1 -1
- package/docs-site/content/benchmark.html +88 -94
- package/docs-site/content/en/benchmark.html +134 -90
- package/docs-site/content/en/index.html +19 -19
- package/docs-site/content/en/runtime.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -1
- package/docs-site/content/en/workflows.html +2 -2
- package/docs-site/content/index.html +19 -19
- package/docs-site/content/runtime.html +4 -4
- package/docs-site/content/whats-new.html +12 -1
- package/docs-site/content/workflows.html +2 -2
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +135 -91
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +20 -20
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +5 -5
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -2
- package/docs-site/en/workflows.html +3 -3
- package/docs-site/index.html +20 -20
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +5 -5
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -2
- package/docs-site/workflows.html +3 -3
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
- package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
- package/evals/fixtures/helper-request.invalid.json +1 -1
- package/evals/fixtures/helper-request.valid.json +1 -1
- package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
- package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +3 -3
- package/evals/fs5-pilot-protocol.v2.json +8 -8
- package/evals/fs5-pilot-protocol.v3.json +8 -8
- package/evals/fs5-pilot-protocol.v4.json +6 -6
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/evals/runtime-conformance-v1/README.md +43 -0
- package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
- package/evals/runtime-conformance-v1/lane.json +57 -0
- package/evals/runtime-conformance-v1/paid-canary.json +31 -0
- package/evals/runtime-conformance-v1/runner.mjs +456 -0
- package/package.json +1 -1
- package/packages/piagent-core/README.md +13 -4
- package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
- package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
- package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
- package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
- package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
- package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
- package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
- package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
- package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
- package/packages/piagent-core/extensions/context-engine.js +1 -11
- package/packages/piagent-core/extensions/context-index-policy.js +4 -0
- package/packages/piagent-core/extensions/context-query-paths.js +57 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
- package/packages/piagent-core/extensions/criterion-graph.js +4 -1
- package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
- package/packages/piagent-core/extensions/guard-types.ts +4 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
- package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
- package/packages/piagent-core/extensions/task-state.js +69 -8
- package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +42 -2
- package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
- package/packages/piagent-core/prompts/be-to-fe.md +2 -8
- package/packages/piagent-core/prompts/plan.md +1 -1
- package/packages/piagent-core/prompts/platform-improve.md +2 -6
- package/packages/piagent-core/prompts/review.md +2 -7
- package/packages/piagent-core/prompts/scout.md +1 -1
- package/packages/piagent-core/prompts/task.md +4 -3
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
- package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
- package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
- package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
- package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
- package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
- package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
- package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
- package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
- package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
- package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
- package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
- package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
- package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
- package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
- package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
- package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
- package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
- package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
- package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
- package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
- package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
- package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
- package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
- package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
- package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
- package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
- package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
- package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
- package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
- package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
- package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
- package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
- package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
- package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
- package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
- package/packages/piagent-core/subagents/piagent-planner.md +6 -4
- package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
- package/packages/piagent-core/subagents/piagent-scout.md +4 -2
- package/packages/piagent-core/subagents/piagent-worker.md +6 -4
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
- package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
- package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
- package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
- package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
- package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
- package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
- package/packages/piagent-webui/server/transcript-projection.ts +91 -5
- package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
- package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
- package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
- package/schemas/project-profile.schema.json +1 -1
- package/schemas/task-contract.schema.json +15 -0
- package/scripts/benchmark-runner-core.mjs +14 -6
- package/scripts/benchmark-runner-finalization.mjs +4 -0
- package/scripts/configure-subagents.sh +42 -34
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/team-doctor.sh +1 -1
- package/scripts/verify-local.sh +5 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
|
|
8
8
|
<meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
|
|
9
9
|
<title>Piagent WebUI</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CgOEyRPG.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-BV-_03yl.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -446,7 +446,9 @@ export default function piagentWebUiExtension(pi: ExtensionAPI): void {
|
|
|
446
446
|
pi.on("tool_execution_end", (event, ctx) => { publishDrafts(ctx, stream.toolEnded(event, bridge.snapshot())); bridgeSoft(() => lifecycle.observeToolEnd(ctx)); });
|
|
447
447
|
pi.on("turn_end", (event, ctx) => publishDrafts(ctx, stream.turnEnded(event, bridge.snapshot())));
|
|
448
448
|
pi.on("agent_settled", (_event, ctx) => {
|
|
449
|
-
|
|
449
|
+
const hasPendingMessages = typeof ctx.hasPendingMessages === "function" ? ctx.hasPendingMessages() : null;
|
|
450
|
+
publishDrafts(ctx, stream.agentSettled(bridge.snapshot(), hasPendingMessages));
|
|
451
|
+
if (hasPendingMessages === true) { publishPersisted(ctx); return; }
|
|
450
452
|
const operationId = bridge.snapshot().identity?.agentOperationId ?? null;
|
|
451
453
|
bridgeSoft(() => bridge.observeAgentSettled(ctx)); bridgeSoft(() => lifecycle.observeAgentSettled(ctx, operationId)); publishPersisted(ctx);
|
|
452
454
|
});
|
|
@@ -317,7 +317,7 @@ export class SameSessionPiBridge {
|
|
|
317
317
|
}
|
|
318
318
|
observeAgentStart(ctx: ExtensionContext): void {
|
|
319
319
|
if (!this.refresh(ctx) || !this.#binding || this.#binding.state !== "ready") return;
|
|
320
|
-
const pending = this.#pending;
|
|
320
|
+
const pending = this.#pending; if (this.#binding.identity.agentOperationId && !pending) return;
|
|
321
321
|
const operationId = pending?.operationId ?? this.#revision("operation", ++this.#operationCounter, "external");
|
|
322
322
|
this.#binding.identity.agentOperationId = operationId; this.#refresh(); this.#event("operation.started", { operationRef: operationId, commandId: pending?.command.commandId });
|
|
323
323
|
const observedEntry = pending ? dispatchEntry(pending, ctx) : null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
|
|
4
|
+
import { classifyToolFailure, handledToolFailure } from "../../piagent-core/runtime/inspection/tool-failure-classification.ts";
|
|
4
5
|
import type { RuntimeEventDraft, RuntimeEventRevision } from "../../piagent-core/runtime/inspection/runtime-event-store.ts";
|
|
5
6
|
import type { BridgeIdentity, BridgeSnapshot } from "./same-session-bridge.ts";
|
|
6
7
|
|
|
@@ -35,6 +36,13 @@ function safeToolName(value: unknown): string {
|
|
|
35
36
|
if (projected.redacted) return "redacted-tool";
|
|
36
37
|
return projected.text.replace(/[^A-Za-z0-9._:-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 128) || "tool";
|
|
37
38
|
}
|
|
39
|
+
function toolClassificationInput(value: unknown): { command: string } | undefined {
|
|
40
|
+
if (!value || typeof value !== "object") return undefined;
|
|
41
|
+
const direct = (value as { command?: unknown }).command;
|
|
42
|
+
const nested = (value as { args?: { command?: unknown } }).args?.command;
|
|
43
|
+
const command = typeof direct === "string" ? direct : typeof nested === "string" ? nested : null;
|
|
44
|
+
return command === null ? undefined : { command: command.slice(0, 8_192) };
|
|
45
|
+
}
|
|
38
46
|
function role(message: any): MessageRole | null {
|
|
39
47
|
return message?.role === "user" ? "user" : message?.role === "assistant" ? "assistant" : message?.role === "toolResult" ? "tool-result" : null;
|
|
40
48
|
}
|
|
@@ -68,10 +76,11 @@ function redaction(applied = false, truncated = false) { return { applied, value
|
|
|
68
76
|
function identityReady(identity: BridgeIdentity | null): identity is BridgeIdentity & { agentOperationId: string } {
|
|
69
77
|
return Boolean(identity?.agentOperationId);
|
|
70
78
|
}
|
|
71
|
-
function activityPayload(state: "started" | "progress" | "finished" | "failed", activityRef: string, toolName: string
|
|
79
|
+
function activityPayload(state: "started" | "progress" | "finished" | "failed", activityRef: string, toolName: string,
|
|
80
|
+
reasonCode: string | null = null) {
|
|
72
81
|
return { state, activityType: "tool", activityRef, toolName, inputDigest: null, outputDigest: null, preview: null, previewKind: "none",
|
|
73
82
|
outputBytes: null, outputLines: null, exitCode: null, isError: state === "finished" ? false : state === "failed" ? true : null,
|
|
74
|
-
affectedFileRefs: [], criterionIds: [], verifierAttemptIds: [], reasonCode
|
|
83
|
+
affectedFileRefs: [], criterionIds: [], verifierAttemptIds: [], reasonCode };
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
export class PiSessionStreamAdapter {
|
|
@@ -81,12 +90,13 @@ export class PiSessionStreamAdapter {
|
|
|
81
90
|
#activeByRole = new Map<MessageRole, ActiveMessage>();
|
|
82
91
|
#lastMessageRef: string | null = null;
|
|
83
92
|
#toolRefs = new Map<string, string>();
|
|
93
|
+
#toolInputs = new Map<string, { command: string } | undefined>();
|
|
84
94
|
#lastToolProgress = new Map<string, number>();
|
|
85
95
|
|
|
86
96
|
constructor(options: { now?: () => Date } = {}) { this.#now = options.now ?? (() => new Date()); }
|
|
87
97
|
reset(): void {
|
|
88
98
|
this.#turnIndex = null; this.#messageCounter = 0; this.#activeByRole.clear(); this.#lastMessageRef = null;
|
|
89
|
-
this.#toolRefs.clear(); this.#lastToolProgress.clear();
|
|
99
|
+
this.#toolRefs.clear(); this.#toolInputs.clear(); this.#lastToolProgress.clear();
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
#base(snapshot: BridgeSnapshot, sourceObservedAt?: unknown): Omit<RuntimeEventDraft, "kind" | "payload" | "evidence" | "redaction"> | null {
|
|
@@ -210,13 +220,14 @@ export class PiSessionStreamAdapter {
|
|
|
210
220
|
const draft = this.#draft(snapshot, "turn.ended", { phase: "ended", finalMessageRef: this.#lastMessageRef, toolResultCount: count, stopReason: reason });
|
|
211
221
|
return draft ? [draft] : [];
|
|
212
222
|
}
|
|
213
|
-
toolStarted(event: { toolCallId?: unknown; toolName?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
|
|
223
|
+
toolStarted(event: { toolCallId?: unknown; toolName?: unknown; args?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
|
|
214
224
|
if (typeof event.toolCallId !== "string" || !event.toolCallId) return [];
|
|
215
225
|
const drafts = this.#activeByRole.get("assistant")
|
|
216
226
|
? this.#drainLines(snapshot, this.#activeByRole.get("assistant") as ActiveMessage, false) : [];
|
|
217
227
|
const name = safeToolName(event.toolName);
|
|
218
228
|
const ref = opaque("tool", [snapshot.identity?.sessionRef, event.toolCallId]), activityRef = opaque("activity", [snapshot.identity?.sessionRef, event.toolCallId]);
|
|
219
|
-
this.#toolRefs.set(event.toolCallId, ref); this.#
|
|
229
|
+
this.#toolRefs.set(event.toolCallId, ref); this.#toolInputs.set(event.toolCallId, toolClassificationInput(event.args));
|
|
230
|
+
this.#lastToolProgress.set(event.toolCallId, 0);
|
|
220
231
|
const draft = this.#draft(snapshot, "activity.started", activityPayload("started", activityRef, name), { toolCallId: ref });
|
|
221
232
|
if (draft) drafts.push(draft);
|
|
222
233
|
return drafts;
|
|
@@ -231,19 +242,31 @@ export class PiSessionStreamAdapter {
|
|
|
231
242
|
const draft = this.#draft(snapshot, "activity.progress", activityPayload("progress", activityRef, name), { toolCallId: ref });
|
|
232
243
|
return draft ? [draft] : [];
|
|
233
244
|
}
|
|
234
|
-
toolEnded(event: { toolCallId?: unknown; toolName?: unknown; isError?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
|
|
245
|
+
toolEnded(event: { toolCallId?: unknown; toolName?: unknown; result?: unknown; isError?: unknown }, snapshot: BridgeSnapshot): RuntimeEventDraft[] {
|
|
235
246
|
if (typeof event.toolCallId !== "string") return [];
|
|
236
247
|
const ref = this.#toolRefs.get(event.toolCallId); if (!ref) return [];
|
|
237
248
|
const name = safeToolName(event.toolName);
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
const
|
|
249
|
+
const reasonCode = classifyToolFailure(name, event.isError === true, event.result, this.#toolInputs.get(event.toolCallId));
|
|
250
|
+
const state = (event.isError === true || reasonCode !== null) && !handledToolFailure(reasonCode) ? "failed" : "finished";
|
|
251
|
+
const activityRef = opaque("activity", [snapshot.identity?.sessionRef, event.toolCallId]);
|
|
252
|
+
this.#toolRefs.delete(event.toolCallId); this.#toolInputs.delete(event.toolCallId); this.#lastToolProgress.delete(event.toolCallId);
|
|
253
|
+
const draft = this.#draft(snapshot, `activity.${state}`, activityPayload(state, activityRef, name, reasonCode), { toolCallId: ref });
|
|
241
254
|
return draft ? [draft] : [];
|
|
242
255
|
}
|
|
243
256
|
agentSettled(snapshot: BridgeSnapshot, hasPendingMessages: boolean | null): RuntimeEventDraft[] {
|
|
257
|
+
// A queued follow-up belongs to the same logical operation. Do not emit a
|
|
258
|
+
// terminal edge or clear stream identity between recovery turns.
|
|
259
|
+
if (hasPendingMessages === true) return [];
|
|
244
260
|
const value = hasPendingMessages === null ? { state: "unknown", value: null, reasonCode: "queue-fact-unavailable" }
|
|
245
261
|
: { state: "known", value: hasPendingMessages, reasonCode: null };
|
|
246
|
-
|
|
262
|
+
// Host-idle only proves that this agent operation stopped producing turns.
|
|
263
|
+
// A governed task is successful only after its durable contract is terminal.
|
|
264
|
+
// Fail closed here so a missed/late completion hook cannot paint an open
|
|
265
|
+
// task as completed in the WebUI.
|
|
266
|
+
const governedTaskOpen = Boolean(snapshot.identity?.taskRunId) && snapshot.taskState !== "terminal";
|
|
267
|
+
const draft = this.#draft(snapshot, "agent-operation.settled", {
|
|
268
|
+
settlement: governedTaskOpen ? "unknown" : "completed", lastStopReason: null, hasPendingMessages: value
|
|
269
|
+
});
|
|
247
270
|
this.reset(); return draft ? [draft] : [];
|
|
248
271
|
}
|
|
249
272
|
}
|
|
@@ -3,6 +3,8 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
4
4
|
import { hasVisibleText } from "../shared/text-visibility.ts";
|
|
5
5
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
6
|
+
import { SessionOperationLifecycle, type SessionOperationObservation,
|
|
7
|
+
type SessionOperationPhase, type SessionOperationRetryPolicyOptions } from "./session-operation-lifecycle.ts";
|
|
6
8
|
|
|
7
9
|
const MAX_BUFFER = 65_536;
|
|
8
10
|
const MAX_EVENTS = 128;
|
|
@@ -73,20 +75,20 @@ export class GatewaySessionStream {
|
|
|
73
75
|
#eventCount = 0;
|
|
74
76
|
#buffer = "";
|
|
75
77
|
#truncated = false;
|
|
76
|
-
#started = false;
|
|
77
78
|
#startListeners: Array<() => void> = [];
|
|
78
|
-
#settled = false;
|
|
79
79
|
#runtimeRestartRequired = false;
|
|
80
80
|
#lastMessageRef: string | null = null;
|
|
81
81
|
#settlement: Settlement = { outcome: "unknown", reasonCode: "operation-settlement-unknown" };
|
|
82
82
|
#forcedSettlement: Settlement | null = null;
|
|
83
83
|
#lifecycleTerminationReasonCode: string | null = null;
|
|
84
|
-
#terminalEmitted = false;
|
|
85
84
|
#settleListeners: Array<() => void> = [];
|
|
86
85
|
readonly #toolRefs = new Map<string, string>();
|
|
86
|
+
readonly #lifecycle: SessionOperationLifecycle;
|
|
87
87
|
|
|
88
88
|
get runtimeRestartRequired(): boolean { return this.#runtimeRestartRequired; }
|
|
89
89
|
get lifecycleTerminationReasonCode(): string | null { return this.#lifecycleTerminationReasonCode; }
|
|
90
|
+
get operationPhase(): SessionOperationPhase { return this.#lifecycle.phase; }
|
|
91
|
+
get retryAbortRequired(): boolean { return this.#lifecycle.retryAbortRequired; }
|
|
90
92
|
|
|
91
93
|
markAborted(reasonCode = "operation-aborted"): void { this.#forcedSettlement = { outcome: "aborted", reasonCode }; }
|
|
92
94
|
markError(reasonCode = "operation-failed"): void {
|
|
@@ -94,60 +96,70 @@ export class GatewaySessionStream {
|
|
|
94
96
|
}
|
|
95
97
|
forceLifecycleTermination(reasonCode: string): void {
|
|
96
98
|
this.#lifecycleTerminationReasonCode ??= reasonCode;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const wasStarted = this.#lifecycle.started, wasSettled = this.#lifecycle.hostSettled;
|
|
100
|
+
this.#lifecycle.forceHostSettlement();
|
|
101
|
+
if (!wasStarted) for (const resolve of this.#startListeners.splice(0)) resolve();
|
|
102
|
+
if (!wasSettled) for (const resolve of this.#settleListeners.splice(0)) resolve();
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
constructor(options: { sessionRef: string; operationRef: string; events: GatewayEventStore
|
|
105
|
+
constructor(options: { sessionRef: string; operationRef: string; events: GatewayEventStore;
|
|
106
|
+
retryPolicy?: SessionOperationRetryPolicyOptions }) {
|
|
102
107
|
this.sessionRef = options.sessionRef; this.operationRef = options.operationRef; this.#events = options.events;
|
|
108
|
+
this.#lifecycle = new SessionOperationLifecycle({ operationRef: options.operationRef, retryPolicy: options.retryPolicy });
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
started(): Promise<void> {
|
|
106
|
-
if (this.#started) return Promise.resolve();
|
|
112
|
+
if (this.#lifecycle.started) return Promise.resolve();
|
|
107
113
|
return new Promise((resolve) => this.#startListeners.push(resolve));
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
settled(): Promise<void> {
|
|
111
|
-
if (this.#
|
|
117
|
+
if (this.#lifecycle.hostSettled) return Promise.resolve();
|
|
112
118
|
return new Promise((resolve) => this.#settleListeners.push(resolve));
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
observe(event: any):
|
|
116
|
-
|
|
121
|
+
observe(event: any): SessionOperationObservation {
|
|
122
|
+
const wasStarted = this.#lifecycle.started, wasSettled = this.#lifecycle.hostSettled;
|
|
123
|
+
const observation = this.#lifecycle.observe(event);
|
|
124
|
+
if (!observation.accepted) return observation;
|
|
125
|
+
if (!wasStarted && this.#lifecycle.started) for (const resolve of this.#startListeners.splice(0)) resolve();
|
|
126
|
+
if (!wasSettled && this.#lifecycle.hostSettled) for (const resolve of this.#settleListeners.splice(0)) resolve();
|
|
127
|
+
if (observation.retry === "abort") this.markError(observation.reasonCode ?? "automatic-retry-blocked");
|
|
117
128
|
if (event?.type === "agent_start") {
|
|
118
|
-
|
|
129
|
+
return observation;
|
|
119
130
|
}
|
|
120
131
|
if (event?.type === "agent_settled") {
|
|
121
|
-
|
|
132
|
+
return observation;
|
|
122
133
|
}
|
|
123
134
|
if (event?.type === "message_start" && event.message?.role === "assistant") {
|
|
124
|
-
this.#messageRef = ref("message"); this.#buffer = ""; return;
|
|
135
|
+
this.#messageRef = ref("message"); this.#buffer = ""; return observation;
|
|
125
136
|
}
|
|
126
137
|
if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta"
|
|
127
138
|
&& typeof event.assistantMessageEvent.delta === "string") {
|
|
128
139
|
const remaining = MAX_BUFFER - this.#buffer.length;
|
|
129
|
-
if (remaining <= 0) { this.#truncated = true; return; }
|
|
140
|
+
if (remaining <= 0) { this.#truncated = true; return observation; }
|
|
130
141
|
this.#buffer += event.assistantMessageEvent.delta.slice(0, remaining);
|
|
131
142
|
if (event.assistantMessageEvent.delta.length > remaining) this.#truncated = true;
|
|
132
|
-
this.#flush(false); return;
|
|
143
|
+
this.#flush(false); return observation;
|
|
133
144
|
}
|
|
134
145
|
if (event?.type === "message_end" && event.message?.role === "assistant") {
|
|
135
|
-
this.#flush(true); this.#lastMessageRef = this.#messageRef; this.#settlement = assistantSettlement(event.message); return;
|
|
146
|
+
this.#flush(true); this.#lastMessageRef = this.#messageRef; this.#settlement = assistantSettlement(event.message); return observation;
|
|
136
147
|
}
|
|
137
148
|
if (event?.type === "tool_execution_start" && typeof event.toolCallId === "string") {
|
|
138
149
|
const toolCallRef = ref("tool"); this.#toolRefs.set(event.toolCallId, toolCallRef);
|
|
139
150
|
this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, toolCallRef,
|
|
140
151
|
toolLabel: safeTool(event.toolName), isError: null, reasonCode: null });
|
|
141
|
-
return;
|
|
152
|
+
return observation;
|
|
142
153
|
}
|
|
143
154
|
if (event?.type === "tool_execution_end" && typeof event.toolCallId === "string") {
|
|
144
|
-
const toolCallRef = this.#toolRefs.get(event.toolCallId); if (!toolCallRef) return;
|
|
155
|
+
const toolCallRef = this.#toolRefs.get(event.toolCallId); if (!toolCallRef) return observation;
|
|
145
156
|
const reasonCode = runtimeRestartReasonCode(event);
|
|
146
157
|
if (reasonCode) this.#runtimeRestartRequired = true;
|
|
147
158
|
this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, toolCallRef,
|
|
148
159
|
toolLabel: safeTool(event.toolName), isError: event.isError === true, reasonCode });
|
|
149
160
|
this.#toolRefs.delete(event.toolCallId);
|
|
150
161
|
}
|
|
162
|
+
return observation;
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
#flush(final: boolean): void {
|
|
@@ -180,9 +192,8 @@ export class GatewaySessionStream {
|
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
complete(sessionRevision: string | null): void {
|
|
184
|
-
if (this.#
|
|
185
|
-
this.#terminalEmitted = true;
|
|
195
|
+
complete(sessionRevision: string | null, taskOutcome: string | null = null): void {
|
|
196
|
+
if (!this.#lifecycle.markTerminal()) return;
|
|
186
197
|
this.#flush(true);
|
|
187
198
|
let settlement = this.#forcedSettlement ?? (this.#runtimeRestartRequired
|
|
188
199
|
? { outcome: "unknown" as const, reasonCode: "runtime-restart-required" }
|
|
@@ -190,6 +201,11 @@ export class GatewaySessionStream {
|
|
|
190
201
|
const messageRef = this.#lastMessageRef ?? this.#messageRef;
|
|
191
202
|
if (settlement.outcome === "completed" && !messageRef) {
|
|
192
203
|
settlement = { outcome: "unknown", reasonCode: "assistant-message-unavailable" };
|
|
204
|
+
} else if (settlement.outcome === "completed" && taskOutcome === "pending") {
|
|
205
|
+
// Provider completion is not governed-task completion. The durable task
|
|
206
|
+
// contract is authoritative, so withhold both success settlement and the
|
|
207
|
+
// durable assistant-success event until that contract becomes terminal.
|
|
208
|
+
settlement = { outcome: "unknown", reasonCode: "task-completion-pending" };
|
|
193
209
|
} else if (settlement.outcome === "completed" && sessionRevision === null) {
|
|
194
210
|
// Streaming text is not durable success until the canonical session
|
|
195
211
|
// projection confirms it. Still terminate the UI, but do not show draft.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { hasVisibleText } from "../shared/text-visibility.ts";
|
|
2
|
+
|
|
3
|
+
export type SessionOperationPhase = "pending" | "running" | "retry" | "compaction" | "settled";
|
|
4
|
+
|
|
5
|
+
export type SessionOperationRetryPolicy = Readonly<{
|
|
6
|
+
maximumAttempts: number;
|
|
7
|
+
maximumDelayMs: number;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type SessionOperationRetryPolicyOptions = Partial<SessionOperationRetryPolicy>;
|
|
11
|
+
|
|
12
|
+
export type SessionOperationObservation = Readonly<{
|
|
13
|
+
accepted: boolean;
|
|
14
|
+
phase: SessionOperationPhase;
|
|
15
|
+
retry: "none" | "allowed" | "abort";
|
|
16
|
+
reasonCode: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
const DEFAULT_RETRY_POLICY: SessionOperationRetryPolicy = Object.freeze({
|
|
20
|
+
// Pi's provider retry is useful before a response exists, but a long retry
|
|
21
|
+
// chain burns quota and keeps the browser in Working. One automatic retry
|
|
22
|
+
// absorbs an ordinary transient failure without replaying a persistent one.
|
|
23
|
+
maximumAttempts: 1,
|
|
24
|
+
maximumDelayMs: 8_000
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function boundedInteger(value: unknown, fallback: number, minimum: number, maximum: number): number {
|
|
28
|
+
if (value === undefined) return fallback;
|
|
29
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
|
|
30
|
+
throw new Error("session-operation-retry-policy-invalid");
|
|
31
|
+
}
|
|
32
|
+
return Number(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sessionOperationRetryPolicy(
|
|
36
|
+
options: SessionOperationRetryPolicyOptions = {}
|
|
37
|
+
): SessionOperationRetryPolicy {
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
maximumAttempts: boundedInteger(options.maximumAttempts, DEFAULT_RETRY_POLICY.maximumAttempts, 0, 10),
|
|
40
|
+
maximumDelayMs: boundedInteger(options.maximumDelayMs, DEFAULT_RETRY_POLICY.maximumDelayMs, 0, 300_000)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function correlationRef(event: any): string | null {
|
|
45
|
+
for (const key of ["operationRef", "agentOperationId", "operationId"]) {
|
|
46
|
+
if (typeof event?.[key] === "string" && event[key]) return event[key];
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function visibleAssistantContent(message: any): boolean {
|
|
52
|
+
if (typeof message?.content === "string") return hasVisibleText(message.content);
|
|
53
|
+
if (!Array.isArray(message?.content)) return false;
|
|
54
|
+
return message.content.some((item: any) => {
|
|
55
|
+
if (item?.type === "text") return hasVisibleText(item.text);
|
|
56
|
+
// Images and every model-emitted tool/server-tool block are observable
|
|
57
|
+
// output even when they do not contain user-visible text.
|
|
58
|
+
return ["image", "image_url", "toolCall", "tool_call", "tool_use", "server_tool_use"]
|
|
59
|
+
.includes(String(item?.type ?? ""));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function attempt(value: unknown): number | null {
|
|
64
|
+
return Number.isSafeInteger(value) && Number(value) >= 1 ? Number(value) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function delay(value: unknown): number | null {
|
|
68
|
+
return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Operation-local authority for provider retry, compaction and settlement.
|
|
73
|
+
*
|
|
74
|
+
* Pi's pinned host owns the actual provider retry. This state machine owns the
|
|
75
|
+
* stricter Piagent admission decision and is shared by the Gateway stream and
|
|
76
|
+
* runtime supervisor. It never treats compaction as a provider retry, and it
|
|
77
|
+
* fails closed once assistant output or a tool call makes replay unsafe.
|
|
78
|
+
*/
|
|
79
|
+
export class SessionOperationLifecycle {
|
|
80
|
+
readonly operationRef: string;
|
|
81
|
+
readonly retryPolicy: SessionOperationRetryPolicy;
|
|
82
|
+
#phase: SessionOperationPhase = "pending";
|
|
83
|
+
#started = false;
|
|
84
|
+
#hostSettled = false;
|
|
85
|
+
#terminal = false;
|
|
86
|
+
#visibleAssistantOutput = false;
|
|
87
|
+
#toolCallObserved = false;
|
|
88
|
+
#lastRetryAttempt = 0;
|
|
89
|
+
#activeRetryAttempt: number | null = null;
|
|
90
|
+
#retryAbortRequired = false;
|
|
91
|
+
|
|
92
|
+
constructor(options: {
|
|
93
|
+
operationRef: string;
|
|
94
|
+
retryPolicy?: SessionOperationRetryPolicyOptions | SessionOperationRetryPolicy;
|
|
95
|
+
}) {
|
|
96
|
+
this.operationRef = options.operationRef;
|
|
97
|
+
this.retryPolicy = sessionOperationRetryPolicy(options.retryPolicy);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get phase(): SessionOperationPhase { return this.#phase; }
|
|
101
|
+
get started(): boolean { return this.#started; }
|
|
102
|
+
get hostSettled(): boolean { return this.#hostSettled; }
|
|
103
|
+
get terminal(): boolean { return this.#terminal; }
|
|
104
|
+
get replayUnsafe(): boolean { return this.#visibleAssistantOutput || this.#toolCallObserved; }
|
|
105
|
+
get retryAbortRequired(): boolean { return this.#retryAbortRequired; }
|
|
106
|
+
|
|
107
|
+
forceHostSettlement(): void {
|
|
108
|
+
this.#started = true;
|
|
109
|
+
this.#hostSettled = true;
|
|
110
|
+
this.#phase = "settled";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
markTerminal(): boolean {
|
|
114
|
+
if (this.#terminal) return false;
|
|
115
|
+
this.#terminal = true;
|
|
116
|
+
this.#hostSettled = true;
|
|
117
|
+
this.#phase = "settled";
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
observe(event: any): SessionOperationObservation {
|
|
122
|
+
if (this.#terminal || this.#hostSettled) return this.#ignored("operation-event-after-settlement");
|
|
123
|
+
const correlated = correlationRef(event);
|
|
124
|
+
if (correlated && correlated !== this.operationRef) return this.#ignored("operation-correlation-mismatch");
|
|
125
|
+
const type = String(event?.type ?? "");
|
|
126
|
+
|
|
127
|
+
if (type === "agent_start") {
|
|
128
|
+
this.#started = true;
|
|
129
|
+
this.#phase = "running";
|
|
130
|
+
return this.#accepted();
|
|
131
|
+
}
|
|
132
|
+
if (type === "agent_settled") {
|
|
133
|
+
this.#started = true;
|
|
134
|
+
this.#hostSettled = true;
|
|
135
|
+
this.#phase = "settled";
|
|
136
|
+
return this.#accepted();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Direct stream fixtures and some deferred command hosts begin with a
|
|
140
|
+
// message/tool event. Treat that as an observed start while still refusing
|
|
141
|
+
// uncorrelated retry/compaction lifecycle events before any operation work.
|
|
142
|
+
if (["message_start", "message_update", "message_end", "tool_execution_start",
|
|
143
|
+
"tool_execution_update", "tool_execution_end", "turn_start", "turn_end"].includes(type)) {
|
|
144
|
+
this.#started = true;
|
|
145
|
+
if (this.#phase === "pending") this.#phase = "running";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (type === "message_update") {
|
|
149
|
+
const update = event?.assistantMessageEvent;
|
|
150
|
+
if (update?.type === "text_delta" && hasVisibleText(update.delta)) this.#visibleAssistantOutput = true;
|
|
151
|
+
if (visibleAssistantContent(event?.message)) this.#visibleAssistantOutput = true;
|
|
152
|
+
return this.#accepted();
|
|
153
|
+
}
|
|
154
|
+
if (type === "message_end" && event?.message?.role === "assistant") {
|
|
155
|
+
if (visibleAssistantContent(event.message)) this.#visibleAssistantOutput = true;
|
|
156
|
+
return this.#accepted();
|
|
157
|
+
}
|
|
158
|
+
if (type === "tool_execution_start") {
|
|
159
|
+
this.#toolCallObserved = true;
|
|
160
|
+
return this.#accepted();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (type === "agent_end" && event?.willRetry === true && this.replayUnsafe) {
|
|
164
|
+
return this.#abortRetry("automatic-retry-replay-unsafe");
|
|
165
|
+
}
|
|
166
|
+
if (type === "auto_retry_start") return this.#retryStarted(event);
|
|
167
|
+
if (type === "auto_retry_end") return this.#retryEnded(event);
|
|
168
|
+
|
|
169
|
+
// Summarization retries belong to compaction and never consume or inherit
|
|
170
|
+
// the provider-turn retry budget.
|
|
171
|
+
if (type === "compaction_start") {
|
|
172
|
+
if (!this.#started) return this.#ignored("operation-event-before-start");
|
|
173
|
+
if (this.#phase === "retry") this.#retryAbortRequired = true;
|
|
174
|
+
this.#phase = "compaction";
|
|
175
|
+
return this.#retryAbortRequired
|
|
176
|
+
? this.#abortRetry("automatic-retry-compaction-overlap")
|
|
177
|
+
: this.#accepted();
|
|
178
|
+
}
|
|
179
|
+
if (type === "compaction_end") {
|
|
180
|
+
if (this.#phase !== "compaction") return this.#ignored("compaction-event-stale");
|
|
181
|
+
this.#phase = "running";
|
|
182
|
+
return this.#accepted();
|
|
183
|
+
}
|
|
184
|
+
if (["summarization_retry_scheduled", "summarization_retry_attempt_start",
|
|
185
|
+
"summarization_retry_finished"].includes(type)) {
|
|
186
|
+
return this.#phase === "compaction" ? this.#accepted() : this.#ignored("compaction-event-stale");
|
|
187
|
+
}
|
|
188
|
+
return this.#accepted();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#retryStarted(event: any): SessionOperationObservation {
|
|
192
|
+
if (!this.#started) return this.#ignored("operation-event-before-start");
|
|
193
|
+
const currentAttempt = attempt(event?.attempt), currentDelay = delay(event?.delayMs);
|
|
194
|
+
if (currentAttempt === null || currentDelay === null) return this.#abortRetry("automatic-retry-metadata-invalid");
|
|
195
|
+
if (currentAttempt === this.#lastRetryAttempt) return this.#ignored("automatic-retry-event-duplicate");
|
|
196
|
+
if (currentAttempt !== this.#lastRetryAttempt + 1) {
|
|
197
|
+
this.#activeRetryAttempt = currentAttempt;
|
|
198
|
+
this.#phase = "retry";
|
|
199
|
+
return this.#abortRetry("automatic-retry-attempt-stale");
|
|
200
|
+
}
|
|
201
|
+
this.#lastRetryAttempt = currentAttempt;
|
|
202
|
+
this.#activeRetryAttempt = currentAttempt;
|
|
203
|
+
if (this.#phase === "compaction") return this.#abortRetry("automatic-retry-compaction-overlap");
|
|
204
|
+
this.#phase = "retry";
|
|
205
|
+
if (this.replayUnsafe) return this.#abortRetry("automatic-retry-replay-unsafe");
|
|
206
|
+
if (currentAttempt > this.retryPolicy.maximumAttempts) return this.#abortRetry("automatic-retry-attempt-limit");
|
|
207
|
+
if (currentDelay > this.retryPolicy.maximumDelayMs) return this.#abortRetry("automatic-retry-delay-limit");
|
|
208
|
+
return { accepted: true, phase: this.#phase, retry: "allowed", reasonCode: null };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#retryEnded(event: any): SessionOperationObservation {
|
|
212
|
+
const currentAttempt = attempt(event?.attempt);
|
|
213
|
+
if (currentAttempt === null || currentAttempt !== this.#activeRetryAttempt) {
|
|
214
|
+
return this.#ignored("automatic-retry-event-stale");
|
|
215
|
+
}
|
|
216
|
+
this.#activeRetryAttempt = null;
|
|
217
|
+
// Keep the highest attempt for the whole logical operation so hosts that
|
|
218
|
+
// emit retry_end between attempts still have a monotonic sequence. Keep an
|
|
219
|
+
// abort latched too: retry_end can arrive synchronously before the
|
|
220
|
+
// supervisor's cancellation microtask observes the host AbortController.
|
|
221
|
+
if (this.#phase === "retry") this.#phase = "running";
|
|
222
|
+
return this.#accepted();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#abortRetry(reasonCode: string): SessionOperationObservation {
|
|
226
|
+
this.#retryAbortRequired = true;
|
|
227
|
+
return { accepted: true, phase: this.#phase, retry: "abort", reasonCode };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#accepted(): SessionOperationObservation {
|
|
231
|
+
return { accepted: true, phase: this.#phase, retry: "none", reasonCode: null };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#ignored(reasonCode: string): SessionOperationObservation {
|
|
235
|
+
return { accepted: false, phase: this.#phase, retry: "none", reasonCode };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type RetryObservation = { retry?: unknown } | null | undefined;
|
|
2
|
+
|
|
3
|
+
function invoke(action: (() => unknown) | null): void {
|
|
4
|
+
if (!action) return;
|
|
5
|
+
try {
|
|
6
|
+
const result = action();
|
|
7
|
+
if (result && typeof (result as PromiseLike<unknown>).then === "function") {
|
|
8
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
9
|
+
}
|
|
10
|
+
} catch { /* the operation watchdog remains the terminal authority */ }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cancels a host-owned provider retry without replaying the prompt. Pinned Pi
|
|
15
|
+
* creates its retry AbortController immediately after auto_retry_start is
|
|
16
|
+
* emitted, so the microtask always runs before even a zero-millisecond timer.
|
|
17
|
+
*/
|
|
18
|
+
export class SessionOperationRetryCancellation {
|
|
19
|
+
readonly #session: any;
|
|
20
|
+
#fallbackIssued = false;
|
|
21
|
+
|
|
22
|
+
constructor(session: any) { this.#session = session; }
|
|
23
|
+
|
|
24
|
+
observe(observation: RetryObservation): void {
|
|
25
|
+
if (observation?.retry !== "abort") return;
|
|
26
|
+
if (typeof this.#session?.abortRetry === "function") {
|
|
27
|
+
const cancel = () => invoke(() => this.#session.abortRetry());
|
|
28
|
+
cancel();
|
|
29
|
+
queueMicrotask(cancel);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Unknown hosts fail closed once. Replaying a tool-capable turn is more
|
|
33
|
+
// dangerous than ending the logical operation with an explicit error.
|
|
34
|
+
if (this.#fallbackIssued || typeof this.#session?.abort !== "function") return;
|
|
35
|
+
this.#fallbackIssued = true;
|
|
36
|
+
invoke(() => this.#session.abort());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SessionOperationRetryCancellation } from "./session-operation-retry-control.ts";
|
|
2
|
+
|
|
1
3
|
export type SessionOperationDeadlineReason = "operation-inactivity-timeout" | "operation-deadline-exceeded";
|
|
2
4
|
|
|
3
5
|
export type SessionOperationDeadlinePolicy = Readonly<{
|
|
@@ -113,10 +115,15 @@ export class SessionOperationWatchdog {
|
|
|
113
115
|
export function armSessionOperationWatchdog(options: {
|
|
114
116
|
watchdog: SessionOperationWatchdog;
|
|
115
117
|
subscribe(listener: (event: unknown) => void): unknown;
|
|
116
|
-
observe(event: unknown):
|
|
118
|
+
observe(event: unknown): unknown;
|
|
119
|
+
retrySession?: any;
|
|
117
120
|
expire(reason: SessionOperationDeadlineReason): void;
|
|
118
121
|
}): () => void {
|
|
119
|
-
const
|
|
122
|
+
const retry = new SessionOperationRetryCancellation(options.retrySession);
|
|
123
|
+
const unsubscribe = options.subscribe((event) => {
|
|
124
|
+
options.watchdog.progress();
|
|
125
|
+
retry.observe(options.observe(event) as { retry?: unknown } | null | undefined);
|
|
126
|
+
});
|
|
120
127
|
if (typeof unsubscribe !== "function") throw new Error("session-operation-subscribe-invalid");
|
|
121
128
|
try { options.watchdog.start(options.expire); }
|
|
122
129
|
catch (error) { try { unsubscribe(); } catch { /* authority is quarantined by the caller */ } throw error; }
|
|
@@ -9,8 +9,7 @@ import { SessionLeaseStore, type SessionLeaseSnapshot } from "./session-lease-st
|
|
|
9
9
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
10
10
|
import { GatewaySessionStream } from "./gateway-session-stream.ts";
|
|
11
11
|
import { executePermissionCommand, executeRuntimeCommand } from "./runtime-session-controls.ts";
|
|
12
|
-
import { configureSessionOptions, effectiveModelRef, effectiveThinkingLevel,
|
|
13
|
-
type EffectiveSessionOptions } from "./session-effective-options.ts";
|
|
12
|
+
import { configureSessionOptions, effectiveModelRef, effectiveThinkingLevel, type EffectiveSessionOptions } from "./session-effective-options.ts";
|
|
14
13
|
import { waitForOperationStart } from "./session-operation-start.ts";
|
|
15
14
|
import { armSessionOperationWatchdog, bestEffortUnsubscribe, boundedResult, sessionOperationDeadlinePolicy, SessionOperationWatchdog, terminateWatchedSessionOperation,
|
|
16
15
|
type SessionOperationDeadlinePolicy, type SessionOperationWatchdogOptions } from "./session-operation-watchdog.ts";
|
|
@@ -133,7 +132,7 @@ export class SessionRuntimeSupervisor {
|
|
|
133
132
|
if (this.#active.has(sessionRef)) throw new Error("session-owner-conflict");
|
|
134
133
|
const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
|
|
135
134
|
const prior = this.#leases.inspect(sessionRef);
|
|
136
|
-
if (prior.state === "gateway-owned" || prior.state === "terminal-owned") {
|
|
135
|
+
if (prior.state === "gateway-owned" || prior.state === "terminal-owned" || prior.state === "recovery-required") {
|
|
137
136
|
try { this.#leases.releaseDeadOwnerForExplicitRecovery(sessionRef); } catch { /* live or unprovable owners stay authoritative */ }
|
|
138
137
|
}
|
|
139
138
|
const lease = this.#leases.acquire(sessionRef, this.#gatewayInstanceRef, runtimeInstanceRef);
|
|
@@ -254,7 +253,7 @@ export class SessionRuntimeSupervisor {
|
|
|
254
253
|
const watchdog = new SessionOperationWatchdog(this.#operationDeadlinePolicy);
|
|
255
254
|
active.operationRef = operationRef; active.stream = stream; active.watchdog = watchdog; active.lastSessionRevision = sessionRevision;
|
|
256
255
|
try {
|
|
257
|
-
active.unsubscribe = armSessionOperationWatchdog({ watchdog, subscribe: (listener) => session.subscribe(listener),
|
|
256
|
+
active.unsubscribe = armSessionOperationWatchdog({ watchdog, subscribe: (listener) => session.subscribe(listener), retrySession: session,
|
|
258
257
|
observe: (event) => stream.observe(event), expire: (reasonCode) => { void this.#terminateOperation(sessionRef,
|
|
259
258
|
operationRef, "error", reasonCode, reasonCode, true).catch(() => undefined); } });
|
|
260
259
|
} catch (error) { stream.markError("session-operation-start-failed");
|
|
@@ -468,11 +467,13 @@ export class SessionRuntimeSupervisor {
|
|
|
468
467
|
} catch { /* Canonical refresh failure cannot be replaced by an invented revision. */ }
|
|
469
468
|
if (this.#active.get(sessionRef) !== active || active.operationRef !== operationRef) return;
|
|
470
469
|
if (this.ownership(sessionRef).state !== "gateway-owned") { stream.markError("session-owner-continuity-lost"); await this.#quarantineRuntime(sessionRef, operationRef, active, stream, "session-owner-continuity-lost"); return; }
|
|
471
|
-
|
|
470
|
+
// The pre-clear projection is stale; derive post-settlement liveness from exact ownership.
|
|
471
|
+
active.operationRef = null; const settledLiveState = this.ownership(sessionRef).liveState;
|
|
472
472
|
if (projection) active.lastSessionRevision = projection.sessionRevision;
|
|
473
|
-
|
|
473
|
+
const taskOutcome = activeSessionTask(active.info.cwd, active.info.id)?.trace?.outcome ?? null;
|
|
474
|
+
stream.complete(projection?.sessionRevision ?? null, taskOutcome);
|
|
474
475
|
if (projection) this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
|
|
475
|
-
liveState: restartRequired ? "uncertain" :
|
|
476
|
+
liveState: restartRequired ? "uncertain" : settledLiveState, operationRef: null,
|
|
476
477
|
reasonCode: restartRequired ? "runtime-restart-required" : null });
|
|
477
478
|
if (this.#active.get(sessionRef) === active) { active.completion = null;
|
|
478
479
|
if (!active.watchdog?.terminating) { active.stream = null; active.watchdog = null; active.settling = false; } }
|
|
@@ -237,8 +237,11 @@ export class SessionLeaseStore {
|
|
|
237
237
|
if (!Number.isFinite(now.getTime())) throw new Error("session-lease-input-invalid");
|
|
238
238
|
return this.#withMutationLock(sessionRef, () => {
|
|
239
239
|
const current = this.inspect(sessionRef);
|
|
240
|
-
const
|
|
241
|
-
|
|
240
|
+
const terminalOwner = current.state === "terminal-owned"
|
|
241
|
+
|| (current.state === "recovery-required" && current.gatewayInstanceRef?.startsWith(TERMINAL_OWNER_PREFIX));
|
|
242
|
+
const pid = terminalOwner ? terminalOwnerPid(current.gatewayInstanceRef)
|
|
243
|
+
: current.state === "gateway-owned" || current.state === "recovery-required"
|
|
244
|
+
? gatewayOwnerPid(current.gatewayInstanceRef) : null;
|
|
242
245
|
if (!pid || processAlive(pid)) throw new Error("session-owner-not-proven-dead");
|
|
243
246
|
this.#append(sessionRef, { recordedAt: now.toISOString(), sessionRef, event: "recovery-required", ownerEpoch: current.ownerEpoch!,
|
|
244
247
|
gatewayInstanceRef: current.gatewayInstanceRef!, runtimeInstanceRef: current.runtimeInstanceRef!, reasonCode: "owner-process-exited" });
|