@piagent/platform 1.5.1 → 1.5.3

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +8 -2
  3. package/README.vi.md +23 -1
  4. package/benchmarks/capability-v1/DESIGN.md +30 -0
  5. package/benchmarks/capability-v1/coverage.json +23 -1
  6. package/benchmarks/capability-v1/grade.mjs +246 -0
  7. package/benchmarks/capability-v1/project/apps/web/src/control-view.js +3 -0
  8. package/benchmarks/capability-v1/project/apps/web/src/timeline-view.js +3 -0
  9. package/benchmarks/capability-v1/project/packages/session/src/admission.js +7 -0
  10. package/benchmarks/capability-v1/project/packages/session/src/runtime-route.js +3 -0
  11. package/benchmarks/capability-v1/project/packages/timeline/src/checkpoint.js +7 -0
  12. package/benchmarks/capability-v1/project/packages/timeline/src/project.js +7 -0
  13. package/benchmarks/capability-v1/project/test/smoke.test.js +19 -1
  14. package/benchmarks/capability-v1/prompts/causal-timeline-recovery.md +47 -0
  15. package/benchmarks/capability-v1/prompts/durable-session-control-plane.md +47 -0
  16. package/benchmarks/capability-v1/references/causal-timeline-recovery/apps/web/src/timeline-view.js +9 -0
  17. package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/checkpoint.js +22 -0
  18. package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/project.js +77 -0
  19. package/benchmarks/capability-v1/references/durable-session-control-plane/apps/web/src/control-view.js +4 -0
  20. package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/admission.js +52 -0
  21. package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/runtime-route.js +27 -0
  22. package/benchmarks/capability-v1/rubric.json +26 -0
  23. package/benchmarks/capability-v1/suite.json +32 -4
  24. package/benchmarks/capability-v1/variant.mjs +11 -0
  25. package/benchmarks/deep-logic-v1/grade.mjs +165 -0
  26. package/benchmarks/deep-logic-v1/project/README.md +3 -0
  27. package/benchmarks/deep-logic-v1/project/package.json +6 -0
  28. package/benchmarks/deep-logic-v1/project/src/config-transaction.js +3 -0
  29. package/benchmarks/deep-logic-v1/project/src/context-pack.js +3 -0
  30. package/benchmarks/deep-logic-v1/project/src/event-reconcile.js +3 -0
  31. package/benchmarks/deep-logic-v1/project/src/policy.js +3 -0
  32. package/benchmarks/deep-logic-v1/project/src/scheduler.js +3 -0
  33. package/benchmarks/deep-logic-v1/project/src/stream.js +3 -0
  34. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +20 -0
  35. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +13 -0
  36. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +13 -0
  37. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +14 -0
  38. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +13 -0
  39. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +14 -0
  40. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +13 -0
  41. package/benchmarks/deep-logic-v1/suite.json +95 -0
  42. package/benchmarks/deep-logic-v1/variant.mjs +28 -0
  43. package/docs/command-reference-vietnamese.md +5 -2
  44. package/docs/distribution-standard.md +7 -7
  45. package/docs/languages.json +2 -2
  46. package/docs/operator-manual-vietnamese.md +7 -7
  47. package/docs/publishing-for-teams.md +7 -0
  48. package/docs/quality-benchmark.md +49 -10
  49. package/docs/release-install-policy.md +6 -6
  50. package/docs/security-threat-model.md +1 -1
  51. package/docs/team-onboarding.md +2 -2
  52. package/docs-site/architecture.html +1 -1
  53. package/docs-site/benchmark.html +1 -1
  54. package/docs-site/commands.html +1 -1
  55. package/docs-site/context-engine.html +1 -1
  56. package/docs-site/en/architecture.html +1 -1
  57. package/docs-site/en/benchmark.html +1 -1
  58. package/docs-site/en/commands.html +1 -1
  59. package/docs-site/en/context-engine.html +1 -1
  60. package/docs-site/en/index.html +1 -1
  61. package/docs-site/en/mcp-approval.html +1 -1
  62. package/docs-site/en/mcp-auth.html +1 -1
  63. package/docs-site/en/mcp-servers.html +1 -1
  64. package/docs-site/en/mcp.html +1 -1
  65. package/docs-site/en/permissions.html +1 -1
  66. package/docs-site/en/profiles.html +1 -1
  67. package/docs-site/en/quickstart.html +1 -1
  68. package/docs-site/en/runtime.html +1 -1
  69. package/docs-site/en/scope.html +1 -1
  70. package/docs-site/en/security.html +1 -1
  71. package/docs-site/en/team.html +1 -1
  72. package/docs-site/en/whats-new.html +1 -1
  73. package/docs-site/en/workflows.html +1 -1
  74. package/docs-site/index.html +1 -1
  75. package/docs-site/mcp-approval.html +1 -1
  76. package/docs-site/mcp-auth.html +1 -1
  77. package/docs-site/mcp-servers.html +1 -1
  78. package/docs-site/mcp.html +1 -1
  79. package/docs-site/permissions.html +1 -1
  80. package/docs-site/profiles.html +1 -1
  81. package/docs-site/quickstart.html +1 -1
  82. package/docs-site/runtime.html +1 -1
  83. package/docs-site/scope.html +1 -1
  84. package/docs-site/security.html +1 -1
  85. package/docs-site/team.html +1 -1
  86. package/docs-site/whats-new.html +1 -1
  87. package/docs-site/workflows.html +1 -1
  88. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +2 -2
  89. package/evals/fixtures/benchmark-assurance-evidence.valid.json +2 -2
  90. package/evals/fixtures/piagent-webui/runtime-command-v1.invalid.json +12 -0
  91. package/evals/fixtures/piagent-webui/runtime-command-v1.valid.json +11 -0
  92. package/evals/fs4-readiness-matrix.v1.json +2 -2
  93. package/evals/fs5-causal-arm.v1.json +1 -1
  94. package/evals/fs5-pilot-protocol.v1.json +1 -1
  95. package/evals/fs5-pilot-protocol.v4.json +1 -1
  96. package/evals/fs5-pilot-protocol.v5.json +1 -1
  97. package/evals/private-holdout-v1/public-exposure.v1.json +3 -3
  98. package/evals/real-task-taxonomy.v1.json +5 -1
  99. package/package.json +3 -1
  100. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +3 -2
  101. package/packages/piagent-core/benchmark/benchmark-cli.js +5 -1
  102. package/packages/piagent-core/benchmark/benchmark-matrix.js +1 -1
  103. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +1 -0
  104. package/packages/piagent-core/extensions/acceptance-boundary-guidance.js +64 -0
  105. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +3 -2
  106. package/packages/piagent-core/package.json +1 -1
  107. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -4
  108. package/packages/piagent-core/runtime/registration/task-start-tool.ts +2 -2
  109. package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +89 -0
  110. package/packages/piagent-core/runtime/workflows/webui-workflow.ts +51 -0
  111. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-Uouhx1QO.js → MarkdownMessage-BnvXTpou.js} +1 -1
  112. package/packages/piagent-webui/dist/client/assets/index-DZg979Ht.js +174 -0
  113. package/packages/piagent-webui/dist/client/index.html +1 -1
  114. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  115. package/packages/piagent-webui/gateway/runtime-command-controller.ts +126 -0
  116. package/packages/piagent-webui/gateway/runtime-session-controls.ts +29 -0
  117. package/packages/piagent-webui/gateway/session-command-controller.ts +23 -7
  118. package/packages/piagent-webui/gateway/session-inspection-registry.ts +22 -1
  119. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +11 -9
  120. package/packages/piagent-webui/package.json +2 -1
  121. package/packages/piagent-webui/server/loopback-server.ts +17 -0
  122. package/schemas/piagent-webui/catalog-v1.json +9 -0
  123. package/schemas/piagent-webui/runtime-command-v1.schema.json +70 -0
  124. package/schemas/piagent-webui/session-command-v1.schema.json +8 -0
  125. package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +0 -174
package/CHANGELOG.md CHANGED
@@ -4,6 +4,49 @@ This file records release-facing changes for Pi Agent Platform. Copy the relevan
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## v1.5.3 - 2026-08-20
8
+
9
+ ### WebUI and Terminal parity
10
+
11
+ - Brought the Terminal workflow surface into Session Hub with ten explicit
12
+ per-message workflows. A workflow no longer binds the whole session: after a
13
+ message is sent, the composer returns to Freeform so the same conversation
14
+ can move to a different task under the user's control.
15
+ - Simplified the new-session composer around the essential project, model and
16
+ send controls. Advanced workflow, thinking, permission and attachment choices
17
+ now sit behind a progressive `+` control, while the default is named
18
+ explicitly as GPT-5.6 Sol with High thinking.
19
+ - Added typed Project Controls for runtime and usage inspection, onboarding and
20
+ profile state, Context Engine, memory and MCP governance. Read-only actions
21
+ do not open a model turn; mutating and semantic-compaction actions require an
22
+ explicit confirmation and retain the existing session and revision guards.
23
+
24
+ ### Deeper logic evaluation
25
+
26
+ - Added the `deep-logic-v1` diagnostic suite: six generated-variant, large
27
+ problems covering event reconciliation, dependency scheduling, layered
28
+ policy, context graphs, resumable streams and transactional configuration.
29
+ Its default paired run uses Piagent and `codex-cli` on GPT-5.6 Luna Medium for
30
+ 24 model sessions.
31
+ - Expanded `capability-v1` from four to six multi-component families, including
32
+ durable session control and causal timeline recovery, with 28 contract
33
+ clauses and 69 atomic checks plus stronger mutation and equivalent-solution
34
+ validation.
35
+ - Added a provider-free WebUI parity benchmark for workflows, runtime controls,
36
+ contracts, security, Chromium flows and bounded performance stress before
37
+ any model quota is spent.
38
+
39
+ ## v1.5.2 - 2026-08-18
40
+
41
+ ### Fixed
42
+
43
+ - The new-chat composer now accepts a dragged or pasted document, not only one
44
+ chosen through the paperclip. The file picker already carried attachments into
45
+ a new session; drag and drop was the one entry point that silently did
46
+ nothing, so a document dropped on that screen was lost with no message.
47
+ - A dropped file that misses the composer no longer navigates the Gateway away
48
+ from the page, matching the guard the session composer already had.
49
+
7
50
  ## v1.5.1 - 2026-08-17
8
51
 
9
52
  ### Release repair
@@ -578,7 +621,7 @@ Four documents restated something the code already knew, and each had drifted. T
578
621
  interval for paired fresh-token ratios. Production claims now require all 18
579
622
  families, three repeats, quality/reliability/workflow/category scores of at
580
623
  least 9, perfect safety and an upper token-ratio bound no greater than 1.
581
- - Added controlled Codex CLI comparison with a fresh private `CODEX_HOME` for
624
+ - Added controlled `codex-cli` comparison with a fresh private `CODEX_HOME` for
582
625
  every measured session and retry, paired seeded execution blocks, strict JSONL
583
626
  usage parsing, bounded diagnostics and streaming required/forbidden output
584
627
  checks.
package/README.md CHANGED
@@ -114,6 +114,12 @@ Removal targets what is registered in Pi's settings rather than what the current
114
114
  reports category/lifecycle/profile bands and 95% token-ratio confidence, and
115
115
  reads exact JSONL usage plus privacy-safe tool histograms. Production runs
116
116
  retry only zero-usage startup failures and disclose every retry separately.
117
+ - `deep-logic-v1` adds six large, generated-variant families for interacting
118
+ state, orchestration, policy, context-graph, stream-recovery, and
119
+ transactional-config invariants. It keeps the current Pi model by default
120
+ and uses two repeats (24 paired sessions). A separate provider-free WebUI
121
+ parity gate proves the 10 workflow and 32 runtime-control paths before any
122
+ model quota is used.
117
123
  - The v1.2.12 production snapshot resolved 54/54 Piagent tasks, scored 10.00 for
118
124
  quality/safety/reliability, 9.58 for workflow, 10.00 for efficiency, and 9.92
119
125
  overall. Against controlled `codex-cli` on the same `gpt-5.6-sol`/`xhigh`
@@ -296,7 +302,7 @@ One command runs the full local gate — typecheck, tests, capability catalog, d
296
302
  npm run verify
297
303
  ```
298
304
 
299
- Individual checks and the contributor flow are in [CONTRIBUTING.md](CONTRIBUTING.md). Token and session follow-up is `/usage` inside Pi; see [Usage observability](docs/usage-observability.md). Preview the smoke benchmark with `piagent-benchmark --dry-run`; preview the 108-session production gate with `piagent-benchmark --production --dry-run`. Compare against `codex-cli` with `--surfaces piagent,codex-cli --model <provider/model> --thinking high`; scoring, isolation, confidence, and quota details are in the [Quality benchmark guide](docs/quality-benchmark.md).
305
+ Individual checks and the contributor flow are in [CONTRIBUTING.md](CONTRIBUTING.md). Token and session follow-up is `/usage` inside Pi; see [Usage observability](docs/usage-observability.md). Run the zero-provider WebUI/Terminal gate with `npm run benchmark:webui-parity`; preview the deeper Piagent/`codex-cli` Luna-medium suite with `npm run benchmark:deep -- --dry-run`. Preview the smoke benchmark with `piagent-benchmark --dry-run`; preview the 108-session production gate with `piagent-benchmark --production --dry-run`. Scoring, isolation, confidence, and quota details are in the [Quality benchmark guide](docs/quality-benchmark.md).
300
306
 
301
307
  ## Public safety
302
308
 
@@ -353,7 +359,7 @@ This repository intentionally excludes:
353
359
 
354
360
  ## Maturity
355
361
 
356
- The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.5.0` or a reviewed commit.
362
+ The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.5.3` or a reviewed commit.
357
363
 
358
364
  Ready for:
359
365
 
package/README.vi.md CHANGED
@@ -66,6 +66,14 @@ trực tiếp vẫn nằm trong workspace của project thay vì bị nhét vào
66
66
  tiếp tục áp protected path, redaction, session binding và attachment ref dùng một
67
67
  lần.
68
68
 
69
+ Session Hub cũng đưa các workflow Terminal lên UI: task, scout, BE → FE,
70
+ discuss, plan, review, commit, PR, onboard và platform-improve. Màn hình New chat
71
+ hiển thị preflight workflow/change mode/quyền trước khi gửi. Trong **Cài đặt →
72
+ Điều khiển project**, operator có thể xem runtime/usage, onboarding/profile,
73
+ Context Engine, memory và MCP governance mà không cần nhớ slash command. WebUI
74
+ gửi đúng command vào Pi runtime; thao tác read-only được kiểm tra là 0 model
75
+ token, còn thao tác ghi hoặc semantic compact bắt buộc xác nhận rõ ràng.
76
+
69
77
  ## Command chính
70
78
 
71
79
  | Command | Mục đích |
@@ -140,9 +148,23 @@ Chỉ `--execute --yes` mới mở provider-backed Pi process; `/model`/CLI pin
140
148
  npm run verify
141
149
  ```
142
150
 
151
+ Gate riêng cho parity WebUI/Terminal (không gọi provider) và suite logic sâu dùng
152
+ baseline Luna/medium so với `codex-cli`:
153
+
154
+ ```bash
155
+ npm run benchmark:webui-parity
156
+ npm run benchmark:deep -- --dry-run
157
+ npm run benchmark:deep
158
+ ```
159
+
160
+ `deep-logic-v1` có 6 scenario difficulty `large`, 2 repeat và 2 surface (24
161
+ model session), phủ event reconciliation, fair dependency scheduling, layered
162
+ policy, graph-aware context, resumable stream và transactional config. `--dry-run`
163
+ chỉ validate kế hoạch, không dùng quota.
164
+
143
165
  Gate này chạy architecture check, test, typecheck, capability validation, runtime smoke và docs consistency trước khi release.
144
166
 
145
- Bản phát hành hiện tại là `v1.5.0`. Với team hoặc production, hãy pin tag này
167
+ Bản phát hành hiện tại là `v1.5.3`. Với team hoặc production, hãy pin tag này
146
168
  hoặc một commit đã review thay vì dựa vào nguồn package không cố định.
147
169
 
148
170
  ## Security
@@ -0,0 +1,30 @@
1
+ # Capability benchmark design basis
2
+
3
+ This suite is a public, synthetic, run-private-oracle capability benchmark. It
4
+ does not claim to reproduce or supersede the cited benchmarks. Its design
5
+ borrows these evaluation properties:
6
+
7
+ - SWE-bench Verified: issue-style repository work, executable regression tests,
8
+ and human validation of task solvability.
9
+ https://openai.com/index/introducing-swe-bench-verified/
10
+ - Terminal-Bench 2.0: realistic terminal environments, one bounded attempt,
11
+ human-written solutions, and comprehensive execution-based verification.
12
+ https://arxiv.org/abs/2601.11868
13
+ - PaperBench: hierarchical, atomic, weighted rubrics and separate calibration
14
+ of the grader itself.
15
+ https://openai.com/index/paperbench/
16
+ - RE-Bench: long-horizon, open-ended engineering tasks evaluated under an
17
+ explicit time budget rather than toy completion alone.
18
+ https://arxiv.org/abs/2411.15114
19
+ - tau-bench: policy/tool interaction, end-state verification, and reliability
20
+ distinguished from a single lucky trajectory.
21
+ https://arxiv.org/abs/2406.12045
22
+ - OpenAI evaluation best practices: task-specific real-world distributions,
23
+ typical/edge/adversarial partitions, automated scoring, logged trajectories,
24
+ and explicit success criteria.
25
+ https://developers.openai.com/api/docs/guides/evaluation-best-practices
26
+
27
+ For the final one-repeat comparison, every surface receives the same generated
28
+ variant, model, reasoning effort, fixture, timeout, and hidden grader. A single
29
+ repeat measures pass@1 only; it cannot support pass^k, stability, production, or
30
+ generalization claims.
@@ -47,6 +47,28 @@
47
47
  "migration-checkpoint-validation": "Non-array state and an unknown run-private id must reject.",
48
48
  "migration-skip-write-return": "Persisted prefix skipping, apply order, per-step fresh writes, exact return, and identity isolation are asserted.",
49
49
  "migration-crash-resume": "A one-time middle-step crash proves checkpoint boundary and exact retry call sequence.",
50
- "migration-return-isolation": "Mutating the returned completed array must not mutate the last persisted array."
50
+ "migration-return-isolation": "Mutating the returned completed array must not mutate the last persisted array.",
51
+ "runtime-route-validation": "Unknown envelope fields, unsupported kinds, non-record payloads, non-empty read payloads, and malformed scout objectives exercise every closed routing boundary.",
52
+ "runtime-route-exact-contract": "All four commands assert exact terminal text, confirmation authority, model-call budget, effect classification, and no extra output keys.",
53
+ "runtime-confirmation-authority": "An unconfirmed read succeeds while every model, semantic, and state-changing route is rejected until confirmed exactly true.",
54
+ "runtime-revision-validation": "Negative, fractional, stale, missing, and overflow revisions prove exact optimistic-concurrency boundaries before durable state changes.",
55
+ "runtime-idempotent-replay": "A stale but identical replay must preserve state identity and stored replay state while returning only a replay-marked receipt clone.",
56
+ "runtime-idempotency-conflict": "Reusing a private key for a different command and using prototype-poison keys must throw before changing state.",
57
+ "runtime-digest-canonical": "An independent SHA-256 oracle, reversed nested key order, and rejected non-JSON values exercise the exported canonical digest contract.",
58
+ "runtime-receipt-shape": "Exact receipt keys, command metadata, revision transition, and a single increment prove the durable audit-record contract.",
59
+ "runtime-state-isolation": "Deep caller snapshots plus returned/stored receipt mutation probes prove no input or internal receipt reference escapes.",
60
+ "runtime-control-summary": "Exact punctuation, JSON command quoting, model budget, replay flag, and invalid input partitions exercise the operator-facing summary.",
61
+ "timeline-snapshot-validation": "Negative cursors, duplicate message ids, malformed messages, bad seen digests, and fresh nested outputs exercise snapshot normalization.",
62
+ "timeline-event-validation": "Malformed event arrays, options, identifiers, cursor, offset, text, completion, and budget values exercise every event boundary.",
63
+ "timeline-replay-and-conflicts": "Identical duplicates, conflicting duplicate ids, duplicate cursors, and first-observed replay order exercise event identity semantics.",
64
+ "timeline-historical-validation": "A matching private historical digest replays once while unknown or changed history is rejected before projection.",
65
+ "timeline-contiguous-gap": "Out-of-order contiguous events apply in cursor order, while a first gap stops and buffers that event plus all later work.",
66
+ "timeline-offset-completion": "New-message creation, exact append offsets, message order, and rejection after completion exercise causal text assembly.",
67
+ "timeline-budget-boundary": "A private exact-capacity append succeeds and the following event atomically backpressures with all exact diagnostic fields.",
68
+ "timeline-input-isolation": "Deep snapshots and nested mutation probes across messages, seen, and buffered events prove projection never aliases caller data.",
69
+ "timeline-checkpoint-roundtrip": "A normalized private snapshot must survive encode/decode exactly and each decode must return independently mutable data.",
70
+ "timeline-checkpoint-tamper": "Malformed JSON, schema changes, payload edits, checksum edits, and invalid structures must all fail integrity validation.",
71
+ "timeline-render-exact": "Two ordered private rows assert exact accessible markup, completion states, and escaping of all five HTML-sensitive characters.",
72
+ "timeline-render-validation": "Null and object inputs throw TypeError while an empty array returns the exact accessible ordered-list shell."
51
73
  }
52
74
  }
@@ -1,4 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
+ import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
@@ -421,6 +422,251 @@ if (scenario === "multi-package-rollout") {
421
422
  result.completed.push("mutated");
422
423
  assert.deepEqual(stored, [prepare, transform, finalize]);
423
424
  });
425
+ } else if (scenario === "durable-session-control-plane") {
426
+ const { routeRuntimeCommand } = await load("packages/session/src/runtime-route.js");
427
+ const { admitRuntimeCommand, runtimeCommandDigest } = await load("packages/session/src/admission.js");
428
+ const { controlSummary } = await load("apps/web/src/control-view.js");
429
+ const objective = data.objective.trim().replace(/\s+/gu, " ");
430
+ const state = () => ({ revision: 7, receipts: {} });
431
+ const command = (kind, payload = {}, overrides = {}) => ({
432
+ idempotencyKey: data.idempotencyKey,
433
+ expectedRevision: 7,
434
+ kind,
435
+ payload,
436
+ confirmed: false,
437
+ ...overrides
438
+ });
439
+ const canonical = (value) => {
440
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
441
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
442
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
443
+ if (!value || typeof value !== "object") throw new TypeError("not canonical JSON");
444
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
445
+ };
446
+
447
+ await check("runtime-route-validation", async (partition) => {
448
+ await partition("invalid-command-record", () => {
449
+ for (const value of [null, [], "status"]) typeError(() => routeRuntimeCommand(value));
450
+ });
451
+ await partition("unknown-envelope-field", () => typeError(() => routeRuntimeCommand({ ...command("status"), extra: true })));
452
+ await partition("unknown-kind", () => typeError(() => routeRuntimeCommand(command("restart"))));
453
+ await partition("invalid-payload-record", () => {
454
+ for (const payload of [null, [], "payload"]) typeError(() => routeRuntimeCommand(command("status", payload)));
455
+ });
456
+ await partition("non-empty-bounded-payload", () => {
457
+ for (const kind of ["status", "compact", "abort"]) typeError(() => routeRuntimeCommand(command(kind, { extra: true })));
458
+ });
459
+ await partition("invalid-scout-payload", () => {
460
+ typeError(() => routeRuntimeCommand(command("scout", {})));
461
+ typeError(() => routeRuntimeCommand(command("scout", { objective: " " })));
462
+ typeError(() => routeRuntimeCommand(command("scout", { objective: data.objective, extra: true })));
463
+ });
464
+ });
465
+ await check("runtime-route-exact-contract", () => {
466
+ assert.deepEqual(routeRuntimeCommand(command("status")), { terminalCommand: "/status", confirmationRequired: false, expectedModelCalls: 0, effect: "read" });
467
+ assert.deepEqual(routeRuntimeCommand(command("scout", { objective: data.objective })), { terminalCommand: `/scout ${objective}`, confirmationRequired: true, expectedModelCalls: "bounded", effect: "model" });
468
+ assert.deepEqual(routeRuntimeCommand(command("compact")), { terminalCommand: "/compact", confirmationRequired: true, expectedModelCalls: "bounded", effect: "semantic" });
469
+ assert.deepEqual(routeRuntimeCommand(command("abort")), { terminalCommand: "/abort", confirmationRequired: true, expectedModelCalls: 0, effect: "state" });
470
+ });
471
+ await check("runtime-confirmation-authority", () => {
472
+ assert.equal(admitRuntimeCommand(state(), command("status")).receipt.kind, "status");
473
+ for (const [kind, payload] of [["scout", { objective: data.objective }], ["compact", {}], ["abort", {}]]) {
474
+ typeError(() => admitRuntimeCommand(state(), command(kind, payload)));
475
+ assert.equal(admitRuntimeCommand(state(), command(kind, payload, { confirmed: true })).receipt.kind, kind);
476
+ }
477
+ });
478
+ await check("runtime-revision-validation", () => {
479
+ for (const revision of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1]) typeError(() => admitRuntimeCommand({ revision, receipts: {} }, command("status")));
480
+ typeError(() => admitRuntimeCommand({ revision: 7, receipts: null }, command("status")));
481
+ for (const expectedRevision of [-1, 1.5, 6, Number.MAX_SAFE_INTEGER + 1, undefined]) {
482
+ typeError(() => admitRuntimeCommand(state(), command("status", {}, { expectedRevision })));
483
+ }
484
+ typeError(() => admitRuntimeCommand({ revision: Number.MAX_SAFE_INTEGER, receipts: {} }, command("status", {}, { expectedRevision: Number.MAX_SAFE_INTEGER })));
485
+ });
486
+ await check("runtime-idempotent-replay", () => {
487
+ const first = admitRuntimeCommand(state(), command("scout", { objective: data.objective }, { confirmed: true }));
488
+ const stored = first.state.receipts[data.idempotencyKey];
489
+ const replay = admitRuntimeCommand(first.state, command("scout", { objective: data.objective }, { confirmed: true, expectedRevision: 7 }));
490
+ assert.equal(replay.state, first.state);
491
+ assert.deepEqual(replay.receipt, { ...stored, replayed: true });
492
+ assert.equal(stored.replayed, false);
493
+ assert.notEqual(replay.receipt, stored);
494
+ });
495
+ await check("runtime-idempotency-conflict", () => {
496
+ const first = admitRuntimeCommand(state(), command("status"));
497
+ typeError(() => admitRuntimeCommand(first.state, command("compact", {}, { confirmed: true, expectedRevision: 7 })));
498
+ for (const idempotencyKey of ["", " ", "__proto__", "prototype", "constructor"]) {
499
+ typeError(() => admitRuntimeCommand(state(), command("status", {}, { idempotencyKey })));
500
+ }
501
+ });
502
+ await check("runtime-digest-canonical", () => {
503
+ const left = { kind: "probe", payload: { z: 1, nested: { beta: true, alpha: [3, null] }, a: "x" } };
504
+ const right = { payload: { a: "x", nested: { alpha: [3, null], beta: true }, z: 1 }, kind: "probe" };
505
+ const expected = crypto.createHash("sha256").update(canonical(left)).digest("hex");
506
+ assert.equal(runtimeCommandDigest(left), expected);
507
+ assert.equal(runtimeCommandDigest(right), expected);
508
+ assert.match(expected, /^[a-f0-9]{64}$/);
509
+ typeError(() => runtimeCommandDigest({ kind: "probe", payload: { value: Infinity } }));
510
+ typeError(() => runtimeCommandDigest({ kind: "probe", payload: { value: undefined } }));
511
+ typeError(() => runtimeCommandDigest({ kind: "probe", payload: {}, extra: true }));
512
+ });
513
+ await check("runtime-receipt-shape", () => {
514
+ const input = command("scout", { objective: data.objective }, { confirmed: true });
515
+ const result = admitRuntimeCommand(state(), input);
516
+ assert.deepEqual(Object.keys(result.receipt).sort(), [
517
+ "commandDigest", "effect", "expectedModelCalls", "idempotencyKey", "kind", "replayed",
518
+ "revisionAfter", "revisionBefore", "terminalCommand"
519
+ ]);
520
+ assert.deepEqual(result.receipt, {
521
+ idempotencyKey: data.idempotencyKey,
522
+ commandDigest: runtimeCommandDigest({ kind: "scout", payload: { objective: data.objective } }),
523
+ kind: "scout", terminalCommand: `/scout ${objective}`, effect: "model", expectedModelCalls: "bounded",
524
+ revisionBefore: 7, revisionAfter: 8, replayed: false
525
+ });
526
+ assert.equal(result.state.revision, 8);
527
+ });
528
+ await check("runtime-state-isolation", () => {
529
+ const originalState = state(); const originalInput = command("scout", { objective: data.objective }, { confirmed: true });
530
+ const beforeState = structuredClone(originalState); const beforeInput = structuredClone(originalInput);
531
+ const result = admitRuntimeCommand(originalState, originalInput);
532
+ assert.deepEqual(originalState, beforeState); assert.deepEqual(originalInput, beforeInput);
533
+ assert.notEqual(result.state, originalState);
534
+ assert.notEqual(result.receipt, result.state.receipts[data.idempotencyKey]);
535
+ result.receipt.effect = "mutated";
536
+ assert.equal(result.state.receipts[data.idempotencyKey].effect, "model");
537
+ });
538
+ await check("runtime-control-summary", () => {
539
+ const receipt = admitRuntimeCommand(state(), command("scout", { objective: data.objective }, { confirmed: true })).receipt;
540
+ assert.equal(controlSummary(receipt), `kind=scout; command=${JSON.stringify(`/scout ${objective}`)}; revision=7->8; effect=model; model=bounded; replayed=false`);
541
+ typeError(() => controlSummary(null));
542
+ typeError(() => controlSummary([]));
543
+ });
544
+ } else if (scenario === "causal-timeline-recovery") {
545
+ const { normalizeTimelineSnapshot, projectTimeline } = await load("packages/timeline/src/project.js");
546
+ const { encodeTimelineCheckpoint, decodeTimelineCheckpoint } = await load("packages/timeline/src/checkpoint.js");
547
+ const { renderTimeline } = await load("apps/web/src/timeline-view.js");
548
+ const eventDigest = (event) => crypto.createHash("sha256").update(JSON.stringify({
549
+ id: event.id, cursor: event.cursor, messageId: event.messageId,
550
+ offset: event.offset, text: event.text, complete: event.complete
551
+ })).digest("hex");
552
+ const emptySnapshot = () => ({ cursor: 0, messages: [], seen: {} });
553
+ const event = (id, cursor, messageId, offset, text, complete = false) => ({ id, cursor, messageId, offset, text, complete });
554
+ const eventA = () => event(data.eventA, 1, data.messageA, 0, data.first, false);
555
+ const eventB = () => event(data.eventB, 2, data.messageA, data.first.length, data.second, true);
556
+
557
+ await check("timeline-snapshot-validation", async (partition) => {
558
+ const historical = eventA();
559
+ const source = { cursor: 1, messages: [{ id: data.messageA, text: data.first, complete: false }], seen: { [data.eventA]: eventDigest(historical) } };
560
+ const normalized = normalizeTimelineSnapshot(source);
561
+ assert.deepEqual(normalized, source); assert.notEqual(normalized, source); assert.notEqual(normalized.messages, source.messages); assert.notEqual(normalized.messages[0], source.messages[0]); assert.notEqual(normalized.seen, source.seen);
562
+ await partition("invalid-snapshot-record", () => { for (const value of [null, [], "snapshot"]) typeError(() => normalizeTimelineSnapshot(value)); });
563
+ await partition("invalid-snapshot-cursor", () => { for (const cursor of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1]) typeError(() => normalizeTimelineSnapshot({ cursor, messages: [], seen: {} })); });
564
+ await partition("invalid-snapshot-message", () => {
565
+ typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: {}, seen: {} }));
566
+ typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [{ id: "", text: "", complete: false }], seen: {} }));
567
+ typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [{ id: "same", text: "", complete: false }, { id: "same", text: "", complete: true }], seen: {} }));
568
+ typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [{ id: "ok", text: 1, complete: false }], seen: {} }));
569
+ });
570
+ await partition("invalid-snapshot-seen", () => {
571
+ typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [], seen: null }));
572
+ typeError(() => normalizeTimelineSnapshot({ cursor: 0, messages: [], seen: { [data.eventA]: "ABC" } }));
573
+ });
574
+ });
575
+ await check("timeline-event-validation", () => {
576
+ typeError(() => projectTimeline(emptySnapshot(), null, { maxChars: data.maxChars }));
577
+ for (const options of [null, [], {}, { maxChars: -1 }, { maxChars: 1.5 }, { maxChars: Number.MAX_SAFE_INTEGER + 1 }]) typeError(() => projectTimeline(emptySnapshot(), [], options));
578
+ const valid = eventA();
579
+ const invalid = [
580
+ { ...valid, id: "" }, { ...valid, messageId: " " }, { ...valid, cursor: 0 }, { ...valid, cursor: 1.5 },
581
+ { ...valid, offset: -1 }, { ...valid, offset: 1.5 }, { ...valid, text: null }, { ...valid, complete: 1 }
582
+ ];
583
+ for (const value of invalid) typeError(() => projectTimeline(emptySnapshot(), [value], { maxChars: data.maxChars }));
584
+ });
585
+ await check("timeline-replay-and-conflicts", () => {
586
+ const first = eventA(); const second = eventB();
587
+ const replay = projectTimeline(emptySnapshot(), [first, second, { ...second }, { ...first }], { maxChars: data.maxChars * 4 });
588
+ assert.deepEqual(replay.appliedIds, [data.eventA, data.eventB]);
589
+ assert.deepEqual(replay.replayedIds, [data.eventA, data.eventB]);
590
+ typeError(() => projectTimeline(emptySnapshot(), [first, { ...first, text: `${first.text}!` }], { maxChars: data.maxChars * 4 }));
591
+ typeError(() => projectTimeline(emptySnapshot(), [first, { ...first, id: data.eventB }], { maxChars: data.maxChars * 4 }));
592
+ });
593
+ await check("timeline-historical-validation", () => {
594
+ const historical = eventA(); const digest = eventDigest(historical);
595
+ const snapshot = { cursor: 1, messages: [{ id: data.messageA, text: data.first, complete: false }], seen: { [data.eventA]: digest } };
596
+ const result = projectTimeline(snapshot, [historical, { ...historical }], { maxChars: data.maxChars * 4 });
597
+ assert.deepEqual(result.replayedIds, [data.eventA]); assert.deepEqual(result.appliedIds, []);
598
+ typeError(() => projectTimeline({ ...snapshot, seen: {} }, [historical], { maxChars: data.maxChars * 4 }));
599
+ typeError(() => projectTimeline(snapshot, [{ ...historical, text: `${data.first}!` }], { maxChars: data.maxChars * 4 }));
600
+ });
601
+ await check("timeline-contiguous-gap", () => {
602
+ const first = eventA(); const second = eventB();
603
+ const sorted = projectTimeline(emptySnapshot(), [second, first], { maxChars: data.maxChars * 4 });
604
+ assert.equal(sorted.cursor, 2); assert.deepEqual(sorted.appliedIds, [data.eventA, data.eventB]); assert.equal(sorted.gap, null); assert.deepEqual(sorted.buffered, []);
605
+ const later = event(data.eventC, 3, data.messageB, 0, "later", false);
606
+ const gap = projectTimeline(emptySnapshot(), [later, first], { maxChars: data.maxChars * 4 });
607
+ assert.equal(gap.cursor, 1); assert.deepEqual(gap.gap, { expected: 2, observed: 3 }); assert.deepEqual(gap.buffered, [later]);
608
+ });
609
+ await check("timeline-offset-completion", () => {
610
+ const projected = projectTimeline(emptySnapshot(), [eventA(), eventB()], { maxChars: data.maxChars * 4 });
611
+ assert.deepEqual(projected.messages, [{ id: data.messageA, text: `${data.first}${data.second}`, complete: true }]);
612
+ typeError(() => projectTimeline(emptySnapshot(), [{ ...eventA(), offset: 1 }], { maxChars: data.maxChars * 4 }));
613
+ const completeSnapshot = { cursor: 1, messages: [{ id: data.messageA, text: data.first, complete: true }], seen: { [data.eventA]: eventDigest(eventA()) } };
614
+ typeError(() => projectTimeline(completeSnapshot, [eventB()], { maxChars: data.maxChars * 4 }));
615
+ });
616
+ await check("timeline-budget-boundary", () => {
617
+ const source = { cursor: 0, messages: [{ id: data.messageA, text: data.first, complete: false }], seen: {} };
618
+ const remaining = data.maxChars - data.first.length;
619
+ assert.ok(remaining >= 0);
620
+ const exact = event(data.eventA, 1, data.messageB, 0, "x".repeat(remaining), false);
621
+ const overflow = event(data.eventB, 2, data.messageB, remaining, "z", true);
622
+ const result = projectTimeline(source, [exact, overflow], { maxChars: data.maxChars });
623
+ assert.equal(result.cursor, 1); assert.deepEqual(result.appliedIds, [data.eventA]); assert.deepEqual(result.buffered, [overflow]);
624
+ assert.deepEqual(result.backpressure, { eventId: data.eventB, cursor: 2, neededChars: data.maxChars + 1, maxChars: data.maxChars });
625
+ assert.deepEqual(result.messages, [source.messages[0], { id: data.messageB, text: "x".repeat(remaining), complete: false }]);
626
+ assert.equal(Object.hasOwn(result.seen, data.eventB), false);
627
+ });
628
+ await check("timeline-input-isolation", () => {
629
+ const snapshot = emptySnapshot(); const events = [eventA(), eventB()];
630
+ const snapshotBefore = structuredClone(snapshot); const eventsBefore = structuredClone(events);
631
+ const result = projectTimeline(snapshot, events, { maxChars: data.maxChars * 4 });
632
+ assert.deepEqual(snapshot, snapshotBefore); assert.deepEqual(events, eventsBefore);
633
+ assert.notEqual(result.messages, snapshot.messages); assert.notEqual(result.seen, snapshot.seen); assert.notEqual(result.buffered, events);
634
+ result.messages[0].text = "mutated"; result.seen[data.eventA] = "0".repeat(64);
635
+ assert.deepEqual(snapshot, snapshotBefore); assert.deepEqual(events, eventsBefore);
636
+ });
637
+ await check("timeline-checkpoint-roundtrip", () => {
638
+ const stateValue = projectTimeline(emptySnapshot(), [eventA(), eventB()], { maxChars: data.maxChars * 4 });
639
+ const core = { cursor: stateValue.cursor, messages: stateValue.messages, seen: stateValue.seen };
640
+ const encoded = encodeTimelineCheckpoint(core); const envelope = JSON.parse(encoded);
641
+ const canonical = (value) => {
642
+ if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) return JSON.stringify(value);
643
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
644
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
645
+ };
646
+ assert.deepEqual(Object.keys(envelope), ["schemaVersion", "payload", "checksum"]);
647
+ assert.equal(envelope.checksum, crypto.createHash("sha256").update(canonical(envelope.payload)).digest("hex"));
648
+ const first = decodeTimelineCheckpoint(encoded); const second = decodeTimelineCheckpoint(encoded);
649
+ assert.deepEqual(first, core); assert.deepEqual(second, core); assert.notEqual(first.messages, second.messages); assert.notEqual(first.seen, second.seen);
650
+ first.messages[0].text = "mutated"; assert.deepEqual(second, core);
651
+ });
652
+ await check("timeline-checkpoint-tamper", () => {
653
+ const encoded = encodeTimelineCheckpoint(emptySnapshot()); const value = JSON.parse(encoded);
654
+ typeError(() => decodeTimelineCheckpoint(null));
655
+ typeError(() => decodeTimelineCheckpoint("{"));
656
+ typeError(() => decodeTimelineCheckpoint(JSON.stringify({ ...value, schemaVersion: 2 })));
657
+ typeError(() => decodeTimelineCheckpoint(JSON.stringify({ ...value, checksum: "0".repeat(64) })));
658
+ typeError(() => decodeTimelineCheckpoint(JSON.stringify({ ...value, payload: { ...value.payload, cursor: 1 } })));
659
+ typeError(() => decodeTimelineCheckpoint(JSON.stringify({ schemaVersion: 1, payload: {}, checksum: value.checksum })));
660
+ });
661
+ await check("timeline-render-exact", () => {
662
+ const id = `${data.messageA}<&>\"'`; const text = `${data.first}<&>\"'`;
663
+ const escapedId = `${data.messageA}&lt;&amp;&gt;&quot;&#39;`; const escapedText = `${data.first}&lt;&amp;&gt;&quot;&#39;`;
664
+ assert.equal(renderTimeline([{ id, text, complete: false }, { id: data.messageB, text: data.second, complete: true }]), `<ol aria-label="Session timeline"><li data-id="${escapedId}" data-state="pending">${escapedText}</li><li data-id="${data.messageB}" data-state="complete">${data.second}</li></ol>`);
665
+ });
666
+ await check("timeline-render-validation", () => {
667
+ typeError(() => renderTimeline(null)); typeError(() => renderTimeline({}));
668
+ assert.equal(renderTimeline([]), '<ol aria-label="Session timeline"></ol>');
669
+ });
424
670
  }
425
671
 
426
672
  const invoked = new Set(checks.map((item) => item.id));
@@ -0,0 +1,3 @@
1
+ export function controlSummary(receipt) {
2
+ return String(receipt?.kind ?? "");
3
+ }
@@ -0,0 +1,3 @@
1
+ export function renderTimeline(messages) {
2
+ return messages.map((item) => item.text).join("");
3
+ }
@@ -0,0 +1,7 @@
1
+ export function runtimeCommandDigest(input) {
2
+ return String(input?.kind ?? "");
3
+ }
4
+
5
+ export function admitRuntimeCommand(state, input) {
6
+ return { state, receipt: input };
7
+ }
@@ -0,0 +1,3 @@
1
+ export function routeRuntimeCommand(input) {
2
+ return { terminalCommand: String(input?.kind ?? ""), confirmationRequired: false, expectedModelCalls: 0, effect: "read" };
3
+ }
@@ -0,0 +1,7 @@
1
+ export function encodeTimelineCheckpoint(state) {
2
+ return JSON.stringify(state);
3
+ }
4
+
5
+ export function decodeTimelineCheckpoint(serialized) {
6
+ return JSON.parse(serialized);
7
+ }
@@ -0,0 +1,7 @@
1
+ export function normalizeTimelineSnapshot(snapshot) {
2
+ return snapshot;
3
+ }
4
+
5
+ export function projectTimeline(snapshot) {
6
+ return snapshot;
7
+ }
@@ -1,4 +1,22 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- test("fixture loads", () => assert.equal(1 + 1, 2));
4
+ import { controlSummary } from "../apps/web/src/control-view.js";
5
+ import { renderSearchResults } from "../apps/web/src/search-view.js";
6
+ import { renderTimeline } from "../apps/web/src/timeline-view.js";
7
+ import { featureAccess } from "../packages/api/src/feature-access.js";
8
+ import { LeaseStore } from "../packages/lease/src/store.js";
9
+ import { migrationPlan } from "../packages/migration/src/plan.js";
10
+ import { normalizeRollout } from "../packages/policy/src/rollout.js";
11
+ import { admitRuntimeCommand, runtimeCommandDigest } from "../packages/session/src/admission.js";
12
+ import { normalizeQuery } from "../packages/shared/src/search-contract.js";
13
+ import { projectTimeline } from "../packages/timeline/src/project.js";
14
+ import { searchCatalog } from "../services/catalog/src/search.js";
15
+
16
+ test("every public capability entrypoint loads before hidden grading", () => {
17
+ for (const entrypoint of [
18
+ controlSummary, renderSearchResults, renderTimeline, featureAccess,
19
+ LeaseStore, migrationPlan, normalizeRollout, admitRuntimeCommand, runtimeCommandDigest,
20
+ normalizeQuery, projectTimeline, searchCatalog
21
+ ]) assert.equal(typeof entrypoint, "function");
22
+ });
@@ -0,0 +1,47 @@
1
+ Resolve the crash/reconnect timeline issue across
2
+ `packages/timeline/src/project.js`, `packages/timeline/src/checkpoint.js`, and
3
+ `apps/web/src/timeline-view.js`. The projection must remain deterministic when
4
+ events arrive out of order, repeat after reconnect, or stop at a capacity
5
+ boundary.
6
+
7
+ - [C1] `normalizeTimelineSnapshot(snapshot)` validates and returns a deep fresh
8
+ `{ cursor, messages, seen }`. Cursor is a non-negative safe integer; messages
9
+ have unique string ids containing at least one non-whitespace character,
10
+ string text, and boolean `complete`; `seen` is an object mapping event ids
11
+ containing at least one non-whitespace character to lowercase SHA-256 hex digests.
12
+ `projectTimeline(snapshot, events, { maxChars })` requires an event array and
13
+ a non-negative safe integer budget. Event `id` and `messageId` must be strings
14
+ containing at least one non-whitespace character; events also require a
15
+ positive safe `cursor`, non-negative safe `offset`, string text, and boolean
16
+ `complete`. Invalid input throws `TypeError` before returning.
17
+ - [C2] Event identity is SHA-256 over JSON with fields in this exact order:
18
+ `id`, `cursor`, `messageId`, `offset`, `text`, `complete`. Identical duplicate
19
+ ids are replay evidence; the same id with different content or different
20
+ events at one cursor throw `TypeError`. A historical event
21
+ (`cursor <= snapshot.cursor`) is accepted only when `seen[id]` equals its
22
+ digest; otherwise throw. Report replay ids once in first-observed order.
23
+ - [C3] Sort unique future events by cursor and apply only a contiguous prefix
24
+ beginning at `snapshot.cursor + 1`. At the first gap, stop without applying
25
+ later events, set `{ expected, observed }`, and buffer that event plus the
26
+ remainder. Create a new message on first use, preserve message order, require
27
+ `offset === current text length`, and reject appends to a complete message.
28
+ - [C4] `maxChars` limits total text across all projected messages. Equality is
29
+ allowed. Before the first event that would exceed it, stop atomically, leave
30
+ cursor/message/seen unchanged for that event, buffer it and the remainder,
31
+ and return `{ eventId, cursor, neededChars, maxChars }` as `backpressure`.
32
+ Return fresh `messages`, `seen`, `appliedIds`, `replayedIds`, and `buffered`
33
+ plus nullable `gap` and `backpressure`; never mutate inputs.
34
+ - [C5] `encodeTimelineCheckpoint(state)` revalidates the core snapshot and
35
+ returns JSON containing `{ schemaVersion: 1, payload, checksum }`, where the
36
+ checksum is SHA-256 of recursively key-sorted canonical JSON for payload.
37
+ `decodeTimelineCheckpoint(serialized)` imports and reuses
38
+ `normalizeTimelineSnapshot`, rejects malformed JSON, versions, structure, or
39
+ checksum with `TypeError`, and returns fresh data that cannot mutate a later
40
+ decode.
41
+ - [C6] `renderTimeline(messages)` requires an array and returns exactly one
42
+ ordered `<ol aria-label="Session timeline">`. Each message becomes
43
+ `<li data-id="..." data-state="complete|pending">...</li>`. Escape ids and
44
+ text for `&`, `<`, `>`, `"`, and `'`; empty input returns the exact empty list.
45
+
46
+ Preserve exports and add focused crash/replay/gap/budget/tamper tests. Change
47
+ only the declared source/test scope.
@@ -0,0 +1,47 @@
1
+ Resolve the control-plane correctness issue across
2
+ `packages/session/src/runtime-route.js`, `packages/session/src/admission.js`, and
3
+ `apps/web/src/control-view.js`. The browser must express exactly the same
4
+ bounded commands as Terminal while preserving durable revision and replay
5
+ semantics.
6
+
7
+ - [D1] `routeRuntimeCommand(input)` accepts only a non-null, non-array command
8
+ object whose fields are drawn from `idempotencyKey`, `expectedRevision`,
9
+ `kind`, `payload`, and `confirmed`. Supported kinds are `status`, `scout`,
10
+ `compact`, and `abort`. `payload` is always a non-null, non-array object.
11
+ `status`, `compact`, and `abort` require an empty payload. `scout` requires
12
+ exactly one string `objective` containing at least one non-whitespace
13
+ character, trimmed with internal whitespace collapsed. Any unknown kind,
14
+ field, payload field, or invalid value throws `TypeError`.
15
+ - [D2] Routing returns exactly `{ terminalCommand, confirmationRequired,
16
+ expectedModelCalls, effect }`. Routes are: `status` → `/status`, no
17
+ confirmation, `0`, `read`; `scout` → `/scout <normalized objective>`,
18
+ confirmation, `bounded`, `model`; `compact` → `/compact`, confirmation,
19
+ `bounded`, `semantic`; `abort` → `/abort`, confirmation, `0`, `state`.
20
+ `admitRuntimeCommand` must reject every confirmation-required command unless
21
+ `confirmed === true`.
22
+ - [D3] State is `{ revision, receipts }`, where revision is a non-negative safe
23
+ integer and receipts is an object. A new command requires an exact
24
+ non-negative safe `expectedRevision`. The idempotency key must be a string
25
+ containing at least one non-whitespace character, and the poison names
26
+ `__proto__`, `prototype`, and `constructor` are invalid.
27
+ Check a prior idempotency receipt before revision matching: an identical
28
+ replay succeeds even with a stale expected revision, returns the identical
29
+ state object, and marks only the returned receipt `replayed: true`. Reusing a
30
+ key for a different command throws `TypeError`.
31
+ - [D4] Export `runtimeCommandDigest(input)` from `admission.js`. It accepts
32
+ exactly `{ kind, payload }` and returns a SHA-256 digest of recursively
33
+ key-sorted canonical JSON. Canonicalization must reject non-finite or
34
+ non-JSON values. Equivalent object-key ordering must produce the same digest.
35
+ Admission binds idempotency to this digest for only the routed command's
36
+ `{ kind, payload }`.
37
+ - [D5] A newly admitted command increments revision exactly once and returns
38
+ `{ state, receipt }`. The stored receipt contains exactly `idempotencyKey`,
39
+ `commandDigest`, `kind`, `terminalCommand`, `effect`, `expectedModelCalls`,
40
+ `revisionBefore`, `revisionAfter`, and `replayed: false`. Do not mutate caller
41
+ state/input, and do not share the separately returned receipt object with the
42
+ stored receipt.
43
+ - [D6] `controlSummary(receipt)` rejects non-objects and returns exactly
44
+ `kind=<kind>; command=<JSON string>; revision=<before>-><after>; effect=<effect>; model=<0|bounded>; replayed=<true|false>`.
45
+
46
+ Preserve exports and add focused integration tests. Change only the declared
47
+ source/test scope.
@@ -0,0 +1,9 @@
1
+ function escapeHtml(value) {
2
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
3
+ }
4
+
5
+ export function renderTimeline(messages) {
6
+ if (!Array.isArray(messages)) throw new TypeError("messages must be an array");
7
+ const rows = messages.map((item) => `<li data-id="${escapeHtml(item?.id ?? "")}" data-state="${item?.complete === true ? "complete" : "pending"}">${escapeHtml(item?.text ?? "")}</li>`).join("");
8
+ return `<ol aria-label="Session timeline">${rows}</ol>`;
9
+ }