@stigmer/server 3.36.0 → 3.37.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/dist/domain/agentexecution/artifacts.d.ts.map +1 -1
- package/dist/domain/agentexecution/artifacts.js +15 -6
- package/dist/domain/agentexecution/artifacts.js.map +1 -1
- package/dist/domain/agentexecution/constants.d.ts +0 -10
- package/dist/domain/agentexecution/constants.d.ts.map +1 -1
- package/dist/domain/agentexecution/constants.js +0 -12
- package/dist/domain/agentexecution/constants.js.map +1 -1
- package/dist/domain/agentexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/agentexecution/lifecycle.js +5 -3
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/agentexecution/platform-rows.d.ts +63 -0
- package/dist/domain/agentexecution/platform-rows.d.ts.map +1 -0
- package/dist/domain/agentexecution/platform-rows.js +106 -0
- package/dist/domain/agentexecution/platform-rows.js.map +1 -0
- package/dist/domain/agentexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/agentexecution/subscribe.js +7 -4
- package/dist/domain/agentexecution/subscribe.js.map +1 -1
- package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
- package/dist/domain/agentexecution/update-status.js +48 -17
- package/dist/domain/agentexecution/update-status.js.map +1 -1
- package/dist/domain/agentexecution/usage.d.ts +0 -2
- package/dist/domain/agentexecution/usage.d.ts.map +1 -1
- package/dist/domain/agentexecution/usage.js +12 -10
- package/dist/domain/agentexecution/usage.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +4 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +30 -8
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/workflow/controller.js +2 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflow/steps.d.ts +7 -4
- package/dist/domain/workflow/steps.d.ts.map +1 -1
- package/dist/domain/workflow/steps.js +29 -17
- package/dist/domain/workflow/steps.js.map +1 -1
- package/dist/domain/workflowexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/workflowexecution/create-steps.js +6 -2
- package/dist/domain/workflowexecution/create-steps.js.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.d.ts.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.js +5 -2
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/domain/workflowexecution/send-signal.d.ts.map +1 -1
- package/dist/domain/workflowexecution/send-signal.js +6 -3
- package/dist/domain/workflowexecution/send-signal.js.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-approval.js.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-file-decision.js +6 -2
- package/dist/domain/workflowexecution/submit-file-decision.js.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.d.ts.map +1 -1
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js +6 -2
- package/dist/domain/workflowexecution/submit-workflow-task-approval.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe-events.js +6 -2
- package/dist/domain/workflowexecution/subscribe-events.js.map +1 -1
- package/dist/domain/workflowexecution/subscribe.d.ts.map +1 -1
- package/dist/domain/workflowexecution/subscribe.js +7 -4
- package/dist/domain/workflowexecution/subscribe.js.map +1 -1
- package/dist/extensions/organization-directory.d.ts +29 -6
- package/dist/extensions/organization-directory.d.ts.map +1 -1
- package/dist/extensions/organization-directory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts +9 -4
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +10 -12
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/temporal/agentexecution/runner-result.d.ts +12 -0
- package/dist/temporal/agentexecution/runner-result.d.ts.map +1 -1
- package/dist/temporal/agentexecution/runner-result.js +15 -0
- package/dist/temporal/agentexecution/runner-result.js.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +44 -22
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/package.json +6 -6
- package/src/domain/agentexecution/__tests__/agentexecution.test.ts +2 -8
- package/src/domain/agentexecution/__tests__/store-faults.test.ts +315 -0
- package/src/domain/agentexecution/__tests__/update-status.test.ts +217 -0
- package/src/domain/agentexecution/artifacts.ts +21 -12
- package/src/domain/agentexecution/constants.ts +0 -15
- package/src/domain/agentexecution/lifecycle.ts +5 -3
- package/src/domain/agentexecution/platform-rows.ts +133 -0
- package/src/domain/agentexecution/subscribe.ts +7 -3
- package/src/domain/agentexecution/update-status.ts +70 -19
- package/src/domain/agentexecution/usage.ts +16 -13
- package/src/domain/organization/controller.ts +47 -10
- package/src/domain/workflow/__tests__/version-audit-degradation.test.ts +192 -0
- package/src/domain/workflow/controller.ts +2 -1
- package/src/domain/workflow/steps.ts +30 -18
- package/src/domain/workflowexecution/__tests__/store-faults.test.ts +418 -0
- package/src/domain/workflowexecution/create-steps.ts +6 -2
- package/src/domain/workflowexecution/lifecycle.ts +5 -2
- package/src/domain/workflowexecution/send-signal.ts +11 -4
- package/src/domain/workflowexecution/submit-approval.ts +6 -2
- package/src/domain/workflowexecution/submit-file-decision.ts +6 -2
- package/src/domain/workflowexecution/submit-workflow-task-approval.ts +6 -2
- package/src/domain/workflowexecution/subscribe-events.ts +6 -2
- package/src/domain/workflowexecution/subscribe.ts +11 -4
- package/src/extensions/__tests__/extension-composition.test.ts +90 -18
- package/src/extensions/organization-directory.ts +33 -7
- package/src/index.ts +5 -1
- package/src/pipeline/__tests__/blind-not-found.test.ts +297 -0
- package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +67 -5
- package/src/pipeline/steps/guard-memory-capture.ts +24 -19
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +51 -1
- package/src/temporal/agentexecution/__tests__/own-behalf-status-writes.test.ts +149 -9
- package/src/temporal/agentexecution/__tests__/replay-histories/runner-failed.json +757 -0
- package/src/temporal/agentexecution/__tests__/replay-histories/user-cancel.json +755 -0
- package/src/temporal/agentexecution/__tests__/replay.test.ts +4 -2
- package/src/temporal/agentexecution/runner-result.ts +16 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +49 -22
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* transcript regression guard (shrink + front-truncation-with-append +
|
|
6
6
|
* the terminal exemption), the equal-length approval finalize, and the
|
|
7
7
|
* presence-guarded runner-owned field pattern (recalled_memories_report).
|
|
8
|
+
* Also pins the platform-row rules (platform-rows.ts, stigmer#980): a
|
|
9
|
+
* platform marker write is appended to the transcript and never replaces
|
|
10
|
+
* it, idempotently, and a PAUSED or CANCELLED execution's stop row stays
|
|
11
|
+
* last across the runner's late writes, whatever order they land in.
|
|
8
12
|
* These exercise applyUpdateStatusMerge directly on a clone — mirroring
|
|
9
13
|
* the freshly-loaded resource the merge mutates in place inside the
|
|
10
14
|
* updateResource write lock.
|
|
@@ -31,6 +35,7 @@ import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexec
|
|
|
31
35
|
|
|
32
36
|
import { createLogger } from "../../../boot/logger.js";
|
|
33
37
|
import { findToolCallInExecution } from "../submit-approval.js";
|
|
38
|
+
import { CANCEL_STOP_ROW, PAUSE_STOP_ROW } from "../platform-rows.js";
|
|
34
39
|
import { applyUpdateStatusMerge } from "../update-status.js";
|
|
35
40
|
|
|
36
41
|
const silentLogger = createLogger({
|
|
@@ -367,3 +372,215 @@ describe("the presence-guarded runner-owned field pattern (recalled_memories_rep
|
|
|
367
372
|
expect(merged.status?.recalledMemoriesReport).toBeUndefined();
|
|
368
373
|
});
|
|
369
374
|
});
|
|
375
|
+
|
|
376
|
+
describe("platform rows (stigmer#980)", () => {
|
|
377
|
+
// The runner's transcript: AI and tool turns, then (for a failure it
|
|
378
|
+
// reports itself) its own system rows.
|
|
379
|
+
function runnerTranscript(...contents: string[]) {
|
|
380
|
+
return contents.map((content) => ({ type: MessageType.MESSAGE_AI, content }));
|
|
381
|
+
}
|
|
382
|
+
function system(...contents: string[]) {
|
|
383
|
+
return contents.map((content) => ({ type: MessageType.MESSAGE_SYSTEM, content }));
|
|
384
|
+
}
|
|
385
|
+
function executionAt(
|
|
386
|
+
phase: ExecutionPhase,
|
|
387
|
+
msgs: MessageInitShape<typeof AgentMessageSchema>[],
|
|
388
|
+
): AgentExecution {
|
|
389
|
+
return create(AgentExecutionSchema, {
|
|
390
|
+
metadata: { id: "exec-rows", name: "exec-rows" },
|
|
391
|
+
spec: {},
|
|
392
|
+
status: { phase, messages: msgs },
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function contents(execution: AgentExecution): string[] {
|
|
396
|
+
return (execution.status?.messages ?? []).map((m) => m.content);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
describe("a platform marker write is appended, never substituted", () => {
|
|
400
|
+
it("appends the cancel row to a transcript already persisted CANCELLED", () => {
|
|
401
|
+
// The Cancel RPC persisted CANCELLED first, then the workflow's
|
|
402
|
+
// cancellation cleanup lands its row: today it replaced the whole
|
|
403
|
+
// transcript with that one line.
|
|
404
|
+
const merged = runBuildStep(
|
|
405
|
+
executionAt(ExecutionPhase.EXECUTION_CANCELLED, runnerTranscript("a1", "a2")),
|
|
406
|
+
{ phase: ExecutionPhase.EXECUTION_CANCELLED, messages: system(CANCEL_STOP_ROW) },
|
|
407
|
+
);
|
|
408
|
+
expect(contents(merged)).toEqual(["a1", "a2", CANCEL_STOP_ROW]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("appends the broken-flow failure lines to a FAILED transcript", () => {
|
|
412
|
+
const merged = runBuildStep(
|
|
413
|
+
executionAt(ExecutionPhase.EXECUTION_FAILED, runnerTranscript("a1", "a2", "a3")),
|
|
414
|
+
{
|
|
415
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
416
|
+
error: "boom",
|
|
417
|
+
messages: system("Internal failure.", "Error details: boom"),
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
expect(contents(merged)).toEqual([
|
|
421
|
+
"a1",
|
|
422
|
+
"a2",
|
|
423
|
+
"a3",
|
|
424
|
+
"Internal failure.",
|
|
425
|
+
"Error details: boom",
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("appends a marker landing before the phase is terminal instead of dropping it to the shrink guard", () => {
|
|
430
|
+
// The runner died without persisting a terminal: the stored phase is
|
|
431
|
+
// still IN_PROGRESS, and the failure lines are shorter than the
|
|
432
|
+
// transcript. Today the shrink guard dropped them.
|
|
433
|
+
const merged = runBuildStep(
|
|
434
|
+
executionAt(ExecutionPhase.EXECUTION_IN_PROGRESS, runnerTranscript("a1", "a2", "a3")),
|
|
435
|
+
{
|
|
436
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
437
|
+
messages: system("Internal failure.", "Error details: boom"),
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
expect(contents(merged)).toEqual([
|
|
441
|
+
"a1",
|
|
442
|
+
"a2",
|
|
443
|
+
"a3",
|
|
444
|
+
"Internal failure.",
|
|
445
|
+
"Error details: boom",
|
|
446
|
+
]);
|
|
447
|
+
expect(merged.status?.phase).toBe(ExecutionPhase.EXECUTION_FAILED);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("is idempotent: an activity retry of the same marker appends nothing", () => {
|
|
451
|
+
const merged = runBuildStep(
|
|
452
|
+
executionAt(ExecutionPhase.EXECUTION_FAILED, [
|
|
453
|
+
...runnerTranscript("a1"),
|
|
454
|
+
...system("Internal failure.", "Error details: boom"),
|
|
455
|
+
]),
|
|
456
|
+
{
|
|
457
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
458
|
+
messages: system("Internal failure.", "Error details: boom"),
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
expect(contents(merged)).toEqual(["a1", "Internal failure.", "Error details: boom"]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("keeps an all-system write that extends the stored transcript a replacement (the runner's own growth)", () => {
|
|
465
|
+
const merged = runBuildStep(
|
|
466
|
+
executionAt(ExecutionPhase.EXECUTION_IN_PROGRESS, system("s1")),
|
|
467
|
+
{ phase: ExecutionPhase.EXECUTION_IN_PROGRESS, messages: system("s1", "s2") },
|
|
468
|
+
);
|
|
469
|
+
expect(contents(merged)).toEqual(["s1", "s2"]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("keeps a system write onto an empty transcript a replacement", () => {
|
|
473
|
+
const merged = runBuildStep(
|
|
474
|
+
executionAt(ExecutionPhase.EXECUTION_IN_PROGRESS, []),
|
|
475
|
+
{ phase: ExecutionPhase.EXECUTION_FAILED, messages: system("Internal failure.") },
|
|
476
|
+
);
|
|
477
|
+
expect(contents(merged)).toEqual(["Internal failure."]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("keeps the runner's full terminal write a replacement (the terminal exemption is untouched)", () => {
|
|
481
|
+
const merged = runBuildStep(
|
|
482
|
+
executionAt(ExecutionPhase.EXECUTION_FAILED, runnerTranscript("a1", "a2")),
|
|
483
|
+
{
|
|
484
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
485
|
+
messages: [...runnerTranscript("a1-final"), ...system("Execution failed: x")],
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
expect(contents(merged)).toEqual(["a1-final", "Execution failed: x"]);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe("a stop row stays last across the runner's late writes", () => {
|
|
493
|
+
it("re-appends the cancel row after a late runner write that extends the transcript", () => {
|
|
494
|
+
// Cancel's three writers race: the RPC's CANCELLED persist and the
|
|
495
|
+
// workflow's row landed; the runner's settle write (it learns of the
|
|
496
|
+
// cancel on its next heartbeat) lands last, carrying no row.
|
|
497
|
+
const merged = runBuildStep(
|
|
498
|
+
executionAt(ExecutionPhase.EXECUTION_CANCELLED, [
|
|
499
|
+
...runnerTranscript("a1"),
|
|
500
|
+
...system(CANCEL_STOP_ROW),
|
|
501
|
+
]),
|
|
502
|
+
{ messages: runnerTranscript("a1", "a2") },
|
|
503
|
+
);
|
|
504
|
+
expect(contents(merged)).toEqual(["a1", "a2", CANCEL_STOP_ROW]);
|
|
505
|
+
expect(merged.status?.phase).toBe(ExecutionPhase.EXECUTION_CANCELLED);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("keeps the pause row last when the runner's settle write lands after it", () => {
|
|
509
|
+
const merged = runBuildStep(
|
|
510
|
+
executionAt(ExecutionPhase.EXECUTION_PAUSED, [
|
|
511
|
+
...runnerTranscript("a1"),
|
|
512
|
+
...system(PAUSE_STOP_ROW),
|
|
513
|
+
]),
|
|
514
|
+
{ messages: runnerTranscript("a1") },
|
|
515
|
+
);
|
|
516
|
+
expect(
|
|
517
|
+
contents(merged),
|
|
518
|
+
"the settle write does not trip the shrink guard on the row, and does not drop it",
|
|
519
|
+
).toEqual(["a1", PAUSE_STOP_ROW]);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("merges a longer settle write under the pause row", () => {
|
|
523
|
+
const merged = runBuildStep(
|
|
524
|
+
executionAt(ExecutionPhase.EXECUTION_PAUSED, [
|
|
525
|
+
...runnerTranscript("a1"),
|
|
526
|
+
...system(PAUSE_STOP_ROW),
|
|
527
|
+
]),
|
|
528
|
+
{ messages: runnerTranscript("a1", "a2", "a3") },
|
|
529
|
+
);
|
|
530
|
+
expect(contents(merged)).toEqual(["a1", "a2", "a3", PAUSE_STOP_ROW]);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("still guards a paused transcript against a real shrink", () => {
|
|
534
|
+
const merged = runBuildStep(
|
|
535
|
+
executionAt(ExecutionPhase.EXECUTION_PAUSED, [
|
|
536
|
+
...runnerTranscript("a1", "a2"),
|
|
537
|
+
...system(PAUSE_STOP_ROW),
|
|
538
|
+
]),
|
|
539
|
+
{ messages: runnerTranscript("a1") },
|
|
540
|
+
);
|
|
541
|
+
expect(contents(merged)).toEqual(["a1", "a2", PAUSE_STOP_ROW]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("does not duplicate the row when a retry of the stop marker lands on it", () => {
|
|
545
|
+
const merged = runBuildStep(
|
|
546
|
+
executionAt(ExecutionPhase.EXECUTION_CANCELLED, [
|
|
547
|
+
...runnerTranscript("a1"),
|
|
548
|
+
...system(CANCEL_STOP_ROW),
|
|
549
|
+
]),
|
|
550
|
+
{ phase: ExecutionPhase.EXECUTION_CANCELLED, messages: system(CANCEL_STOP_ROW) },
|
|
551
|
+
);
|
|
552
|
+
expect(contents(merged)).toEqual(["a1", CANCEL_STOP_ROW]);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("treats the pause row as ordinary history once a resume moved the phase on", () => {
|
|
556
|
+
// The resumed runner seeds the persisted transcript, row included,
|
|
557
|
+
// and grows it; nothing is held back or re-appended.
|
|
558
|
+
const merged = runBuildStep(
|
|
559
|
+
executionAt(ExecutionPhase.EXECUTION_IN_PROGRESS, [
|
|
560
|
+
...runnerTranscript("a1"),
|
|
561
|
+
...system(PAUSE_STOP_ROW),
|
|
562
|
+
]),
|
|
563
|
+
{
|
|
564
|
+
phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
565
|
+
messages: [
|
|
566
|
+
...runnerTranscript("a1"),
|
|
567
|
+
...system(PAUSE_STOP_ROW),
|
|
568
|
+
...runnerTranscript("a2"),
|
|
569
|
+
],
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
expect(contents(merged)).toEqual(["a1", PAUSE_STOP_ROW, "a2"]);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("never holds a row the phase does not own (a pause row on a CANCELLED execution is history)", () => {
|
|
576
|
+
const merged = runBuildStep(
|
|
577
|
+
executionAt(ExecutionPhase.EXECUTION_CANCELLED, [
|
|
578
|
+
...runnerTranscript("a1"),
|
|
579
|
+
...system(PAUSE_STOP_ROW),
|
|
580
|
+
]),
|
|
581
|
+
{ messages: [...runnerTranscript("a1"), ...system(PAUSE_STOP_ROW), ...runnerTranscript("a2")] },
|
|
582
|
+
);
|
|
583
|
+
expect(contents(merged)).toEqual(["a1", PAUSE_STOP_ROW, "a2"]);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
invalidArgumentError,
|
|
58
58
|
} from "../../pipeline/errors.js";
|
|
59
59
|
import { authorizeDirect } from "../../pipeline/steps/authorize.js";
|
|
60
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
60
61
|
import type { Store } from "../../store/interface.js";
|
|
61
62
|
import { casBlobContentMismatch } from "./filereview/cas-blob.js";
|
|
62
63
|
|
|
@@ -254,18 +255,22 @@ export async function getArtifactContent(
|
|
|
254
255
|
throw invalidArgumentError("storage_key does not belong to this execution");
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
// Existence check
|
|
258
|
+
// Existence check: only a missing execution is NotFound; any other store
|
|
259
|
+
// failure is an infrastructure fault.
|
|
258
260
|
try {
|
|
259
261
|
await deps.store.getResource(
|
|
260
262
|
ApiResourceKind.agent_execution,
|
|
261
263
|
req.executionId,
|
|
262
264
|
AgentExecutionSchema,
|
|
263
265
|
);
|
|
264
|
-
} catch {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof ResourceNotFoundError) {
|
|
268
|
+
throw new ConnectError(
|
|
269
|
+
`execution not found: ${req.executionId}`,
|
|
270
|
+
Code.NotFound,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
throw internalError(error, "failed to load agent execution");
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
let maxBytes = Number(req.maxBytes);
|
|
@@ -424,7 +429,8 @@ export async function getArtifactDownloadUrl(
|
|
|
424
429
|
);
|
|
425
430
|
|
|
426
431
|
// Load BEFORE the key check: the attachment arm needs spec.attachments,
|
|
427
|
-
// and this doubles as the existence check
|
|
432
|
+
// and this doubles as the existence check (a missing execution is
|
|
433
|
+
// NotFound; any other store failure is an infrastructure fault).
|
|
428
434
|
let execution: AgentExecution;
|
|
429
435
|
try {
|
|
430
436
|
execution = await deps.store.getResource(
|
|
@@ -432,11 +438,14 @@ export async function getArtifactDownloadUrl(
|
|
|
432
438
|
req.executionId,
|
|
433
439
|
AgentExecutionSchema,
|
|
434
440
|
);
|
|
435
|
-
} catch {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof ResourceNotFoundError) {
|
|
443
|
+
throw new ConnectError(
|
|
444
|
+
`execution not found: ${req.executionId}`,
|
|
445
|
+
Code.NotFound,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
throw internalError(error, "failed to load agent execution");
|
|
440
449
|
}
|
|
441
450
|
|
|
442
451
|
// Same normalize-before-check posture as getArtifactContent
|
|
@@ -14,21 +14,6 @@
|
|
|
14
14
|
export const ENGINE_UNAVAILABLE_MESSAGE =
|
|
15
15
|
"The execution engine is temporarily unavailable. Please try again shortly.";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* getExecutionUsageReport's unknown-execution refusal. Go
|
|
19
|
-
* (get_execution_usage_report.go) calls
|
|
20
|
-
* NotFoundError("agent execution '%s' not found", executionID) — but that
|
|
21
|
-
* helper's signature is (resource, id) rendering "%s not found: %s", so
|
|
22
|
-
* the literal '%s' and the doubled "not found" reach the wire. Ported
|
|
23
|
-
* byte-faithfully per sub-project DD-001 (owner-ratified 2026-08-24);
|
|
24
|
-
* the both-editions fix is stigmer/stigmer#859.
|
|
25
|
-
*/
|
|
26
|
-
export function executionUsageReportNotFoundMessage(
|
|
27
|
-
executionId: string,
|
|
28
|
-
): string {
|
|
29
|
-
return `agent execution '%s' not found not found: ${executionId}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
17
|
/**
|
|
33
18
|
* The run gate's deny copy per request shape (P1 sp.run-gate, ruling
|
|
34
19
|
* Q-RG-4; wire once merged, asserted by the conformance run-gate suite).
|
|
@@ -135,9 +135,11 @@ function newLoadExecutionByIdStep<Desc extends DescMessage>(
|
|
|
135
135
|
executionId,
|
|
136
136
|
AgentExecutionSchema,
|
|
137
137
|
);
|
|
138
|
-
} catch {
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof ResourceNotFoundError) {
|
|
140
|
+
throw notFoundError("agent_execution", executionId);
|
|
141
|
+
}
|
|
142
|
+
throw internalError(error, "failed to load agent execution");
|
|
141
143
|
}
|
|
142
144
|
ctx.set(LOADED_EXECUTION_KEY, execution);
|
|
143
145
|
},
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform rows: the MESSAGE_SYSTEM lines the server itself writes into
|
|
3
|
+
* an execution's transcript, and the transcript rules that keep them.
|
|
4
|
+
* The runner owns the transcript; these are the few rows only the
|
|
5
|
+
* platform can write, because only the platform knows the fact they
|
|
6
|
+
* state (stigmer#980).
|
|
7
|
+
*
|
|
8
|
+
* - Stop rows say why a run stopped when the stop came from outside the
|
|
9
|
+
* turn. The runner cannot tell a Pause from a Cancel: both reach its
|
|
10
|
+
* activity as the same Temporal cancellation (reason CANCELLED), and
|
|
11
|
+
* both RPCs signal the engine before they persist the phase
|
|
12
|
+
* (lifecycle.ts), so no stored fact distinguishes them in time. The
|
|
13
|
+
* invoke workflow knows which stop it is running, so it writes the row
|
|
14
|
+
* (workflows/invoke-agent-execution.ts) and the runner writes none.
|
|
15
|
+
* - A platform marker write is a status update whose messages are all
|
|
16
|
+
* MESSAGE_SYSTEM and do not extend the stored transcript: the
|
|
17
|
+
* workflow's stop rows and its broken-flow failure lines. A marker is
|
|
18
|
+
* appended, never substituted for the transcript. Every runner write
|
|
19
|
+
* is a superset of the stored transcript (a resumed runner seeds from
|
|
20
|
+
* it, runner/src/harness/turn-context.ts) and the runner only ever
|
|
21
|
+
* appends system rows, so a runner write never reads as a marker.
|
|
22
|
+
*
|
|
23
|
+
* Messages carry no id (agentexecution/v1/message.proto), so rows are
|
|
24
|
+
* identified by type and content: the stop copy is these exact
|
|
25
|
+
* constants, defined once here for the workflow and the merge, and never
|
|
26
|
+
* matched against copy some other component authors.
|
|
27
|
+
*
|
|
28
|
+
* Pure (protos only): the workflow bundle imports this module by direct
|
|
29
|
+
* path (src/temporal/README.md, workflow-bundle import discipline).
|
|
30
|
+
*/
|
|
31
|
+
import {
|
|
32
|
+
ExecutionPhase,
|
|
33
|
+
MessageType,
|
|
34
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
35
|
+
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
36
|
+
|
|
37
|
+
/** The stop row of a paused execution. Byte-identical to the copy the runner wrote before stigmer#980 moved it here. */
|
|
38
|
+
export const PAUSE_STOP_ROW =
|
|
39
|
+
"Execution paused by user. Use resume to continue.";
|
|
40
|
+
|
|
41
|
+
/** The stop row of a cancelled execution: a quiet terminal state, never a failure (stigmer#282). */
|
|
42
|
+
export const CANCEL_STOP_ROW = "Execution was cancelled.";
|
|
43
|
+
|
|
44
|
+
/** The stop row a phase carries, or undefined for a phase the platform does not explain. */
|
|
45
|
+
export function stopRowForPhase(phase: ExecutionPhase): string | undefined {
|
|
46
|
+
switch (phase) {
|
|
47
|
+
case ExecutionPhase.EXECUTION_PAUSED:
|
|
48
|
+
return PAUSE_STOP_ROW;
|
|
49
|
+
case ExecutionPhase.EXECUTION_CANCELLED:
|
|
50
|
+
return CANCEL_STOP_ROW;
|
|
51
|
+
default:
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether two rows are the same row: messages carry no id, so identity is type and content. */
|
|
57
|
+
function sameRow(a: AgentMessage, b: AgentMessage): boolean {
|
|
58
|
+
return a.type === b.type && a.content === b.content;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether transcript ends with tail, row for row. */
|
|
62
|
+
export function endsWithRows(
|
|
63
|
+
transcript: readonly AgentMessage[],
|
|
64
|
+
tail: readonly AgentMessage[],
|
|
65
|
+
): boolean {
|
|
66
|
+
if (tail.length > transcript.length) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const offset = transcript.length - tail.length;
|
|
70
|
+
return tail.every((row, i) => sameRow(transcript[offset + i], row));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether incoming is a platform marker write against existing: every
|
|
75
|
+
* incoming row is MESSAGE_SYSTEM, and it does not extend a non-empty
|
|
76
|
+
* stored transcript (existing is not a row-for-row prefix of incoming).
|
|
77
|
+
* An all-system write that does extend it is the runner's own growth; a
|
|
78
|
+
* write onto an empty transcript has nothing to preserve.
|
|
79
|
+
*/
|
|
80
|
+
export function isPlatformMarkerWrite(
|
|
81
|
+
existing: readonly AgentMessage[],
|
|
82
|
+
incoming: readonly AgentMessage[],
|
|
83
|
+
): boolean {
|
|
84
|
+
if (existing.length === 0 || incoming.length === 0) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (!incoming.every((row) => row.type === MessageType.MESSAGE_SYSTEM)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const extendsExisting =
|
|
91
|
+
incoming.length >= existing.length &&
|
|
92
|
+
existing.every((row, i) => sameRow(row, incoming[i]));
|
|
93
|
+
return !extendsExisting;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Appends a marker write's rows to the transcript; a retry of a marker
|
|
98
|
+
* already at the transcript's end appends nothing, so the workflow's
|
|
99
|
+
* activity retries stay idempotent.
|
|
100
|
+
*/
|
|
101
|
+
export function appendPlatformMarker(
|
|
102
|
+
existing: readonly AgentMessage[],
|
|
103
|
+
marker: readonly AgentMessage[],
|
|
104
|
+
): AgentMessage[] {
|
|
105
|
+
if (endsWithRows(existing, marker)) {
|
|
106
|
+
return [...existing];
|
|
107
|
+
}
|
|
108
|
+
return [...existing, ...marker];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The stop row the merge holds last: the stored transcript's final row
|
|
113
|
+
* when it is the stored phase's own stop row. While the execution rests
|
|
114
|
+
* in that phase, the row is the platform's, not part of what the runner's
|
|
115
|
+
* later writes replace; once the phase moves on (a resume), it is
|
|
116
|
+
* ordinary history and nothing is held.
|
|
117
|
+
*/
|
|
118
|
+
export function heldStopRow(
|
|
119
|
+
phase: ExecutionPhase,
|
|
120
|
+
transcript: readonly AgentMessage[],
|
|
121
|
+
): AgentMessage | undefined {
|
|
122
|
+
const copy = stopRowForPhase(phase);
|
|
123
|
+
const last = transcript[transcript.length - 1];
|
|
124
|
+
if (
|
|
125
|
+
copy === undefined ||
|
|
126
|
+
last === undefined ||
|
|
127
|
+
last.type !== MessageType.MESSAGE_SYSTEM ||
|
|
128
|
+
last.content !== copy
|
|
129
|
+
) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return last;
|
|
133
|
+
}
|
|
@@ -46,10 +46,12 @@ import type { Logger } from "../../boot/logger.js";
|
|
|
46
46
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
47
47
|
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
48
48
|
import {
|
|
49
|
+
internalError,
|
|
49
50
|
invalidArgumentError,
|
|
50
51
|
notFoundError,
|
|
51
52
|
} from "../../pipeline/errors.js";
|
|
52
53
|
import { authorizeDirect } from "../../pipeline/steps/authorize.js";
|
|
54
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
53
55
|
import type { Store } from "../../store/interface.js";
|
|
54
56
|
|
|
55
57
|
import { isTranscriptTerminalPhase } from "./phases.js";
|
|
@@ -97,9 +99,11 @@ export async function* subscribeExecution(
|
|
|
97
99
|
id,
|
|
98
100
|
AgentExecutionSchema,
|
|
99
101
|
);
|
|
100
|
-
} catch {
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof ResourceNotFoundError) {
|
|
104
|
+
throw notFoundError("AgentExecution", id);
|
|
105
|
+
}
|
|
106
|
+
throw internalError(error, "failed to load agent execution");
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
yield snapshot;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* UpdateStatus — ports controller/update_status.go: the runner's
|
|
3
3
|
* progressive status-update RPC and the domain's single merge chokepoint.
|
|
4
4
|
*
|
|
5
|
-
* Chain
|
|
6
|
-
* NotifyStatusObservers → BroadcastToStreams. The merge+persist is ONE
|
|
5
|
+
* Chain: Authorize → ValidateUpdateStatusInput → MergeAndPersistExecution
|
|
6
|
+
* → NotifyStatusObservers → BroadcastToStreams. The merge+persist is ONE
|
|
7
7
|
* atomic read-modify-write under the store's per-resource write lock
|
|
8
8
|
* (store.updateResource) — the same discipline SubmitApproval uses,
|
|
9
9
|
* load-bearing now that the append-only approval_event_stream is the
|
|
@@ -24,10 +24,14 @@
|
|
|
24
24
|
* wholesale with the runner's latest complete state, while server-owned
|
|
25
25
|
* fields (approval decisions, the approval_event_stream, the file_review
|
|
26
26
|
* ledger) are preserved/authored here because the runner never sends
|
|
27
|
-
* them.
|
|
27
|
+
* them. The transcript's platform rows are the same kind of server-owned
|
|
28
|
+
* state (platform-rows.ts, stigmer#980): a platform marker write is
|
|
29
|
+
* appended and never replaces the transcript, and a PAUSED or CANCELLED
|
|
30
|
+
* execution's stop row stays last across the runner's later writes,
|
|
31
|
+
* because the Pause and Cancel RPCs, the invoke workflow and the runner
|
|
32
|
+
* all write a stopped execution and nothing orders them.
|
|
28
33
|
*
|
|
29
|
-
*
|
|
30
|
-
* Publish steps (broadcast rides in-memory channels per ADR 011).
|
|
34
|
+
* Broadcast rides in-memory channels (ADR 011).
|
|
31
35
|
*/
|
|
32
36
|
import { create } from "@bufbuild/protobuf";
|
|
33
37
|
|
|
@@ -78,6 +82,12 @@ import {
|
|
|
78
82
|
isTerminalExecutionPhase,
|
|
79
83
|
isTranscriptTerminalPhase,
|
|
80
84
|
} from "./phases.js";
|
|
85
|
+
import {
|
|
86
|
+
appendPlatformMarker,
|
|
87
|
+
endsWithRows,
|
|
88
|
+
heldStopRow,
|
|
89
|
+
isPlatformMarkerWrite,
|
|
90
|
+
} from "./platform-rows.js";
|
|
81
91
|
import {
|
|
82
92
|
applyResponseDecorators,
|
|
83
93
|
notifyStatusObservers,
|
|
@@ -257,6 +267,54 @@ export function nonTerminalTranscriptRegression(
|
|
|
257
267
|
return { reject: false, reason: "" };
|
|
258
268
|
}
|
|
259
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The transcript the merge persists, or undefined to keep the stored one.
|
|
272
|
+
* The stored phase's stop row, when it closes the transcript, is held
|
|
273
|
+
* aside so the runner's settle write (which never carries it) is judged
|
|
274
|
+
* against the transcript beneath it, and re-appended after. A platform
|
|
275
|
+
* marker write is appended; anything else is the runner's write, replaced
|
|
276
|
+
* wholesale unless the regression guard rejects it.
|
|
277
|
+
*/
|
|
278
|
+
function mergeTranscript(
|
|
279
|
+
existingPhase: ExecutionPhase,
|
|
280
|
+
existing: AgentMessage[],
|
|
281
|
+
incoming: AgentMessage[],
|
|
282
|
+
executionId: string,
|
|
283
|
+
logger: Logger,
|
|
284
|
+
): AgentMessage[] | undefined {
|
|
285
|
+
const held = heldStopRow(existingPhase, existing);
|
|
286
|
+
const base = held === undefined ? existing : existing.slice(0, -1);
|
|
287
|
+
|
|
288
|
+
let next: AgentMessage[];
|
|
289
|
+
if (isPlatformMarkerWrite(base, incoming)) {
|
|
290
|
+
next = appendPlatformMarker(base, incoming);
|
|
291
|
+
} else {
|
|
292
|
+
const { reject, reason } = nonTerminalTranscriptRegression(
|
|
293
|
+
existingPhase,
|
|
294
|
+
base,
|
|
295
|
+
incoming,
|
|
296
|
+
);
|
|
297
|
+
if (reject) {
|
|
298
|
+
logger.warn(
|
|
299
|
+
"Rejected status update that would drop committed transcript history for a non-terminal execution; keeping existing messages",
|
|
300
|
+
{
|
|
301
|
+
executionId,
|
|
302
|
+
existingMessages: existing.length,
|
|
303
|
+
incomingMessages: incoming.length,
|
|
304
|
+
reason,
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
next = incoming;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (held !== undefined && !endsWithRows(next, [held])) {
|
|
313
|
+
next = [...next, held];
|
|
314
|
+
}
|
|
315
|
+
return next;
|
|
316
|
+
}
|
|
317
|
+
|
|
260
318
|
/**
|
|
261
319
|
* Merges an incoming status update into the execution in place — the
|
|
262
320
|
* runner-owns-the-transcript merge rules, exactly update_status.go
|
|
@@ -294,7 +352,8 @@ export function applyUpdateStatusMerge(
|
|
|
294
352
|
// any update that would drop committed transcript history for a
|
|
295
353
|
// non-terminal execution. The runner owns the transcript and only ever
|
|
296
354
|
// GROWS it in flight; two regressions are rejected at this single
|
|
297
|
-
// persistence chokepoint
|
|
355
|
+
// persistence chokepoint (the platform's own rows are handled first,
|
|
356
|
+
// see mergeTranscript):
|
|
298
357
|
// 1. A strictly SHORTER transcript — the classic partial write.
|
|
299
358
|
// 2. A transcript that DROPS a committed tool-call id while appending
|
|
300
359
|
// enough later turns to keep the count equal-or-greater
|
|
@@ -305,23 +364,15 @@ export function applyUpdateStatusMerge(
|
|
|
305
364
|
// post-denial narration redaction), so a content check would reject
|
|
306
365
|
// valid writes.
|
|
307
366
|
if (requestStatus.messages.length > 0) {
|
|
308
|
-
const
|
|
367
|
+
const merged = mergeTranscript(
|
|
309
368
|
existingPhase,
|
|
310
369
|
existingMessages,
|
|
311
370
|
requestStatus.messages,
|
|
371
|
+
input.executionId,
|
|
372
|
+
logger,
|
|
312
373
|
);
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
"Rejected status update that would drop committed transcript history for a non-terminal execution; keeping existing messages",
|
|
316
|
-
{
|
|
317
|
-
executionId: input.executionId,
|
|
318
|
-
existingMessages: existingMessages.length,
|
|
319
|
-
incomingMessages: requestStatus.messages.length,
|
|
320
|
-
reason,
|
|
321
|
-
},
|
|
322
|
-
);
|
|
323
|
-
} else {
|
|
324
|
-
status.messages = requestStatus.messages;
|
|
374
|
+
if (merged !== undefined) {
|
|
375
|
+
status.messages = merged;
|
|
325
376
|
}
|
|
326
377
|
}
|
|
327
378
|
|