@kynetic-ai/spec 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -455
- package/dist/agent-runtime/bootstrap.d.ts +31 -0
- package/dist/agent-runtime/bootstrap.d.ts.map +1 -0
- package/dist/agent-runtime/bootstrap.js +302 -0
- package/dist/agent-runtime/bootstrap.js.map +1 -0
- package/dist/agent-runtime/dispatch.d.ts +119 -10
- package/dist/agent-runtime/dispatch.d.ts.map +1 -1
- package/dist/agent-runtime/dispatch.js +1154 -219
- package/dist/agent-runtime/dispatch.js.map +1 -1
- package/dist/agent-runtime/invocation.d.ts +28 -1
- package/dist/agent-runtime/invocation.d.ts.map +1 -1
- package/dist/agent-runtime/invocation.js +171 -59
- package/dist/agent-runtime/invocation.js.map +1 -1
- package/dist/agent-runtime/prompts.d.ts +9 -0
- package/dist/agent-runtime/prompts.d.ts.map +1 -1
- package/dist/agent-runtime/prompts.js +42 -7
- package/dist/agent-runtime/prompts.js.map +1 -1
- package/dist/agent-runtime/session-event-accumulator.d.ts +83 -0
- package/dist/agent-runtime/session-event-accumulator.d.ts.map +1 -0
- package/dist/agent-runtime/session-event-accumulator.js +203 -0
- package/dist/agent-runtime/session-event-accumulator.js.map +1 -0
- package/dist/agent-runtime/session-event-types.d.ts +67 -0
- package/dist/agent-runtime/session-event-types.d.ts.map +1 -0
- package/dist/agent-runtime/session-event-types.js +13 -0
- package/dist/agent-runtime/session-event-types.js.map +1 -0
- package/dist/agent-runtime/workspace.d.ts +244 -0
- package/dist/agent-runtime/workspace.d.ts.map +1 -0
- package/dist/agent-runtime/workspace.js +2025 -0
- package/dist/agent-runtime/workspace.js.map +1 -0
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +58 -13
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/spawner.d.ts +8 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +25 -3
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-exec.js +1 -1
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/command-annotations.d.ts +15 -3
- package/dist/cli/command-annotations.d.ts.map +1 -1
- package/dist/cli/command-annotations.js +23 -3
- package/dist/cli/command-annotations.js.map +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +144 -27
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +5 -5
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +118 -3
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/guard.d.ts.map +1 -1
- package/dist/cli/commands/guard.js +8 -6
- package/dist/cli/commands/guard.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +20 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +205 -47
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +24 -10
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts +3 -3
- package/dist/cli/commands/plan-import.d.ts.map +1 -1
- package/dist/cli/commands/plan-import.js +213 -528
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +533 -83
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/review.d.ts +14 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +1142 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +1 -0
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +33 -10
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts +2 -4
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
- package/dist/cli/commands/session/checkpoint.js +6 -107
- package/dist/cli/commands/session/checkpoint.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +33 -23
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/compact.js +4 -4
- package/dist/cli/commands/session/compact.js.map +1 -1
- package/dist/cli/commands/session/create.js +2 -2
- package/dist/cli/commands/session/create.js.map +1 -1
- package/dist/cli/commands/session/format.d.ts.map +1 -1
- package/dist/cli/commands/session/format.js +1 -6
- package/dist/cli/commands/session/format.js.map +1 -1
- package/dist/cli/commands/session/log.d.ts +32 -7
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +166 -60
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/session/migrate.d.ts +9 -0
- package/dist/cli/commands/session/migrate.d.ts.map +1 -0
- package/dist/cli/commands/session/migrate.js +46 -0
- package/dist/cli/commands/session/migrate.js.map +1 -0
- package/dist/cli/commands/session/stale-close.d.ts.map +1 -1
- package/dist/cli/commands/session/stale-close.js +5 -8
- package/dist/cli/commands/session/stale-close.js.map +1 -1
- package/dist/cli/commands/session/types.d.ts +1 -1
- package/dist/cli/commands/session/types.d.ts.map +1 -1
- package/dist/cli/commands/setup.d.ts +2 -2
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +287 -257
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/shadow.d.ts.map +1 -1
- package/dist/cli/commands/shadow.js +147 -31
- package/dist/cli/commands/shadow.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts +7 -0
- package/dist/cli/commands/skill-crud.d.ts.map +1 -1
- package/dist/cli/commands/skill-crud.js +41 -18
- package/dist/cli/commands/skill-crud.js.map +1 -1
- package/dist/cli/commands/skill-diff.d.ts.map +1 -1
- package/dist/cli/commands/skill-diff.js +29 -3
- package/dist/cli/commands/skill-diff.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +5 -4
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +359 -49
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/trait.d.ts.map +1 -1
- package/dist/cli/commands/trait.js +5 -27
- package/dist/cli/commands/trait.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +113 -52
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +69 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +108 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/sync-mode.d.ts +44 -0
- package/dist/cli/sync-mode.d.ts.map +1 -0
- package/dist/cli/sync-mode.js +64 -0
- package/dist/cli/sync-mode.js.map +1 -0
- package/dist/daemon/middleware/project-context.ts +25 -7
- package/dist/daemon/project-context.ts +18 -0
- package/dist/daemon/routes/agent-dispatch.ts +99 -22
- package/dist/daemon/routes/aggregation.ts +184 -0
- package/dist/daemon/routes/inbox.ts +5 -0
- package/dist/daemon/routes/items.ts +145 -0
- package/dist/daemon/routes/meta.ts +1 -1
- package/dist/daemon/routes/projects.ts +28 -6
- package/dist/daemon/routes/ref-resolution.ts +119 -0
- package/dist/daemon/routes/refs.ts +42 -0
- package/dist/daemon/routes/session-related.ts +140 -0
- package/dist/daemon/routes/sessions.ts +420 -19
- package/dist/daemon/routes/tasks.ts +62 -5
- package/dist/daemon/routes/triage.ts +40 -1
- package/dist/daemon/server.ts +143 -49
- package/dist/daemon/session-sync.ts +11 -0
- package/dist/daemon/shadow-sync.ts +11 -0
- package/dist/daemon/watcher.ts +56 -5
- package/dist/daemon/websocket/project-resolution.ts +77 -0
- package/dist/export/json.d.ts.map +1 -1
- package/dist/export/json.js +104 -1
- package/dist/export/json.js.map +1 -1
- package/dist/export/types.d.ts +52 -1
- package/dist/export/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parser/agent-detection.d.ts +1 -1
- package/dist/parser/agent-detection.d.ts.map +1 -1
- package/dist/parser/agent-detection.js +10 -0
- package/dist/parser/agent-detection.js.map +1 -1
- package/dist/parser/config.d.ts +397 -2
- package/dist/parser/config.d.ts.map +1 -1
- package/dist/parser/config.js +125 -3
- package/dist/parser/config.js.map +1 -1
- package/dist/parser/dispatch-workspaces.d.ts +18 -0
- package/dist/parser/dispatch-workspaces.d.ts.map +1 -0
- package/dist/parser/dispatch-workspaces.js +209 -0
- package/dist/parser/dispatch-workspaces.js.map +1 -0
- package/dist/parser/doctor.d.ts.map +1 -1
- package/dist/parser/doctor.js +27 -8
- package/dist/parser/doctor.js.map +1 -1
- package/dist/parser/file-lock.d.ts.map +1 -1
- package/dist/parser/file-lock.js +9 -2
- package/dist/parser/file-lock.js.map +1 -1
- package/dist/parser/index.d.ts +6 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +6 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +1 -0
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/refs.d.ts +8 -1
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +27 -1
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/review-operations.d.ts +72 -0
- package/dist/parser/review-operations.d.ts.map +1 -0
- package/dist/parser/review-operations.js +185 -0
- package/dist/parser/review-operations.js.map +1 -0
- package/dist/parser/review-task-integration.d.ts +78 -0
- package/dist/parser/review-task-integration.d.ts.map +1 -0
- package/dist/parser/review-task-integration.js +173 -0
- package/dist/parser/review-task-integration.js.map +1 -0
- package/dist/parser/review-threads.d.ts +101 -0
- package/dist/parser/review-threads.d.ts.map +1 -0
- package/dist/parser/review-threads.js +222 -0
- package/dist/parser/review-threads.js.map +1 -0
- package/dist/parser/review-validation.d.ts +69 -0
- package/dist/parser/review-validation.d.ts.map +1 -0
- package/dist/parser/review-validation.js +207 -0
- package/dist/parser/review-validation.js.map +1 -0
- package/dist/parser/reviews.d.ts +58 -0
- package/dist/parser/reviews.d.ts.map +1 -0
- package/dist/parser/reviews.js +230 -0
- package/dist/parser/reviews.js.map +1 -0
- package/dist/parser/session-branch.d.ts +91 -0
- package/dist/parser/session-branch.d.ts.map +1 -0
- package/dist/parser/session-branch.js +565 -0
- package/dist/parser/session-branch.js.map +1 -0
- package/dist/parser/session-sync-scheduler.d.ts +53 -0
- package/dist/parser/session-sync-scheduler.d.ts.map +1 -0
- package/dist/parser/session-sync-scheduler.js +100 -0
- package/dist/parser/session-sync-scheduler.js.map +1 -0
- package/dist/parser/setup-status.d.ts +7 -1
- package/dist/parser/setup-status.d.ts.map +1 -1
- package/dist/parser/setup-status.js +104 -39
- package/dist/parser/setup-status.js.map +1 -1
- package/dist/parser/shadow-sync-scheduler.d.ts +71 -0
- package/dist/parser/shadow-sync-scheduler.d.ts.map +1 -0
- package/dist/parser/shadow-sync-scheduler.js +139 -0
- package/dist/parser/shadow-sync-scheduler.js.map +1 -0
- package/dist/parser/shadow.d.ts +121 -14
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +752 -27
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +24 -0
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +98 -26
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/parser/validate.d.ts +43 -3
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +204 -30
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +47 -11
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +329 -149
- package/dist/parser/yaml.js.map +1 -1
- package/dist/review/checks.d.ts +97 -0
- package/dist/review/checks.d.ts.map +1 -0
- package/dist/review/checks.js +175 -0
- package/dist/review/checks.js.map +1 -0
- package/dist/review/index.d.ts +3 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/index.js +3 -0
- package/dist/review/index.js.map +1 -0
- package/dist/review/subject-bindings.d.ts +83 -0
- package/dist/review/subject-bindings.d.ts.map +1 -0
- package/dist/review/subject-bindings.js +175 -0
- package/dist/review/subject-bindings.js.map +1 -0
- package/dist/schema/common.d.ts +26 -0
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +13 -0
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/dispatch-workspace.d.ts +2643 -0
- package/dist/schema/dispatch-workspace.d.ts.map +1 -0
- package/dist/schema/dispatch-workspace.js +187 -0
- package/dist/schema/dispatch-workspace.js.map +1 -0
- package/dist/schema/inbox.d.ts +8 -8
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/meta.d.ts +648 -116
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +27 -0
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +30 -19
- package/dist/schema/plan.d.ts.map +1 -1
- package/dist/schema/plan.js +3 -1
- package/dist/schema/plan.js.map +1 -1
- package/dist/schema/review-records.d.ts +2676 -0
- package/dist/schema/review-records.d.ts.map +1 -0
- package/dist/schema/review-records.js +232 -0
- package/dist/schema/review-records.js.map +1 -0
- package/dist/schema/spec.d.ts +32 -14
- package/dist/schema/spec.d.ts.map +1 -1
- package/dist/schema/spec.js +5 -0
- package/dist/schema/spec.js.map +1 -1
- package/dist/schema/task.d.ts +187 -29
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +12 -2
- package/dist/schema/task.js.map +1 -1
- package/dist/schema/triage.d.ts +22 -22
- package/dist/sessions/cache.d.ts +119 -0
- package/dist/sessions/cache.d.ts.map +1 -0
- package/dist/sessions/cache.js +284 -0
- package/dist/sessions/cache.js.map +1 -0
- package/dist/sessions/index.d.ts +1 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/legacy.d.ts +77 -0
- package/dist/sessions/legacy.d.ts.map +1 -0
- package/dist/sessions/legacy.js +146 -0
- package/dist/sessions/legacy.js.map +1 -0
- package/dist/sessions/store.d.ts +103 -73
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +335 -186
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +44 -16
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +11 -2
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +32 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +17 -0
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/labels.d.ts +1 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +1 -0
- package/dist/strings/labels.js.map +1 -1
- package/dist/utils/activity.d.ts +101 -0
- package/dist/utils/activity.d.ts.map +1 -0
- package/dist/utils/activity.js +408 -0
- package/dist/utils/activity.js.map +1 -0
- package/dist/utils/git.d.ts +31 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +87 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/web-ui/_app/immutable/assets/0.tmlwn-Ih.css +1 -0
- package/dist/web-ui/_app/immutable/assets/9.BwwJybWx.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/2KqE8gtn.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/70-t_QvE.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/AiWQj974.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{CPPfDSei.js → B25nWFyA.js} +4 -4
- package/dist/web-ui/_app/immutable/chunks/{DBYE9jOd.js → B2bcA_Q_.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/B5e5HYyB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B7-5z6eA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B7bGmhK0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{DzO4hlg9.js → B8tYZKAE.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/{B5LJFxqa.js → BFGAyJjD.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/BG0850zf.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{DAMmvwn4.js → BG8eSzAd.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/BIMxXS8I.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BSzL1fpU.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BYtjHfeq.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{DxCk-KHc.js → Bp5pFYXL.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/{B8a0xDxR.js → BsJFsuAT.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/BvpNHcD6.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BypqA25-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{BVA9Exy-.js → C0w6WDm5.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/C5_PAZ0y.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CDRO15Iv.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CF1CoqD5.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CS2sa4_m.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{BJ0JX3ea.js → CWUQwB9H.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/CY5FDdSU.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C_7MTDoj.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{D3vxvonu.js → CaAJD3dl.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/{BP352uRn.js → ChB5iyEL.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/{pE6cYWlS.js → ChQD-6N8.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/{Eo4gF7ih.js → CqbsoCwA.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/DCeJW50p.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{Cncwi6fQ.js → DJtZNgcs.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/DKIeaprD.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DLd2uVIA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{DjcCz-PU.js → DW_subyT.js} +2 -2
- package/dist/web-ui/_app/immutable/chunks/DbU6lVn0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Dc7ZCC5m.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Dd5umPsk.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/{BysXJlZb.js → Dg_zDpDS.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/Dgqu8Yuc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DmxsPZTB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DphTaFUB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DqK4iHp0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{D9QNBZM2.js → DqT6OH_u.js} +2 -2
- package/dist/web-ui/_app/immutable/chunks/Ds9I9wQb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Du5ng3u4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DxJw79Wi.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/GFTX8GgV.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{C076q4JN.js → HNjs76Zz.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/HVMjDi4_.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{BkOJ8DkV.js → P0A_fJvS.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/T3vGWjIL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/VTmrX9Qu.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{k_Qegko0.js → Xvwhx_F1.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/Yyz1XMQA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{62JVKtnb.js → dh5HeqUr.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/fZMteyca.js +62 -0
- package/dist/web-ui/_app/immutable/chunks/{D82RulSH.js → gPrj-hqC.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/htcWMiYN.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{CwELQvbx.js → oTsvd9y4.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/qJfLUwU4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/xCtiO_JE.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{DvA-KON-.js → y4GeEH6k.js} +1 -1
- package/dist/web-ui/_app/immutable/entry/app.C4h_eOn6.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.CQFTf9ep.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.Dh1xO970.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.l75D3Opx.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/10.DBidBPc-.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/11.Ab0gUKWe.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/12.CMsnoxfs.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/13.D8YKuknB.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/14.DZ0aan7y.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/15.CUIKreDL.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/16.BWc8--BO.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.CDUonbuh.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.Ctg3M00i.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.Ci-JDwbA.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/5.CTyEDAq0.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.BTZZqsAb.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.BI52g_Jo.js +137 -0
- package/dist/web-ui/_app/immutable/nodes/8.3hZPaB9x.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/9.DS49kvwl.js +29 -0
- package/dist/web-ui/_app/version.json +1 -1
- package/dist/web-ui/favicon-192.png +0 -0
- package/dist/web-ui/favicon-32.png +0 -0
- package/dist/web-ui/favicon.ico +0 -0
- package/dist/web-ui/index.html +14 -14
- package/package.json +12 -6
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/merge/SKILL.md +127 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +55 -26
- package/plugin/plugins/kspec/skills/review/SKILL.md +350 -133
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +96 -106
- package/templates/agents-sections/04-pr-workflow.md +15 -12
- package/templates/agents-sections/06-ralph-loop.md +15 -10
- package/templates/skills/manifest.yaml +25 -7
- package/templates/skills/merge/SKILL.md +120 -0
- package/templates/skills/plan/SKILL.md +55 -26
- package/templates/skills/review/SKILL.md +346 -130
- package/templates/skills/task-work/SKILL.md +93 -103
- package/dist/web-ui/_app/immutable/assets/0.BJaYkGW2.css +0 -1
- package/dist/web-ui/_app/immutable/assets/9.SzGLxi4x.css +0 -1
- package/dist/web-ui/_app/immutable/chunks/-lc0BifF.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/8RBjHMN1.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/B5wTVqxm.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/B6VSmczZ.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BEOQc37C.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BHtYorjv.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BMuCqDX8.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BUZujXJ2.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BWET-efb.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BXkNecpt.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BYzrIfX8.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BpuwufMc.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BwMO4RrG.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/C33JaVbg.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/CGtqifKp.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/CHDZZ7OG.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/CUir3f4J.js +0 -60
- package/dist/web-ui/_app/immutable/chunks/CrCIbn0C.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/D6TVmR9T.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/D7LTux4W.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DAh4Wfku.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DAx07bEQ.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DOno4cA2.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DQA8NZIH.js +0 -2
- package/dist/web-ui/_app/immutable/chunks/DRfPm2bo.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DhQhksaB.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DjG7s6hm.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DkltRNvh.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DlaTnPKL.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/ExCq5swK.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/T3zZGv51.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/XZumBYeP.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/_ySfNjkF.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/iEtR5cV6.js +0 -1
- package/dist/web-ui/_app/immutable/entry/app.Cgu6uKeS.js +0 -2
- package/dist/web-ui/_app/immutable/entry/start.9XifnLoB.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/0.DISwcKSK.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/1.Cx2Ufqp1.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/10.C3z8ijXL.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/11.DZdIjZmM.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/12.FsIGfAOa.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/13.DZoFwagf.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/14.DaIzDKbQ.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/15.BYyt4XWF.js +0 -2
- package/dist/web-ui/_app/immutable/nodes/16.CQkSqpOe.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/2.Bkf_j2UJ.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/3.kaMCurJG.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/4.BSsFPTHG.js +0 -2
- package/dist/web-ui/_app/immutable/nodes/5.CpPlcCEZ.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/6.BN4FqQmY.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/7.9kBYIZik.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/8.BuijtZ6B.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/9.C-Weba8R.js +0 -1
package/dist/sessions/store.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Session metadata management
|
|
8
8
|
*
|
|
9
9
|
* Storage structure:
|
|
10
|
-
* .kspec
|
|
10
|
+
* .kspec-sessions/{session-id}/
|
|
11
11
|
* session.yaml # Metadata
|
|
12
12
|
* events.jsonl # Append-only event log
|
|
13
13
|
*/
|
|
@@ -17,10 +17,9 @@ import * as path from "node:path";
|
|
|
17
17
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
18
|
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
19
19
|
import * as YAML from "yaml";
|
|
20
|
-
import { shadowAutoCommit } from "../parser/shadow.js";
|
|
21
20
|
import { SessionEventSchema, SessionMetadataSchema, TaskBudgetSchema, } from "./types.js";
|
|
21
|
+
import { sessionBranchAutoCommit, } from "../parser/session-branch.js";
|
|
22
22
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
23
|
-
const SESSIONS_DIR = "sessions";
|
|
24
23
|
const METADATA_FILE = "session.yaml";
|
|
25
24
|
const EVENTS_FILE = "events.jsonl";
|
|
26
25
|
const BUDGET_FILE = "budget.json";
|
|
@@ -31,64 +30,104 @@ const EVENT_LINE_MAX_BYTES = 256 * 1024;
|
|
|
31
30
|
const EVENT_FIELD_EXTERNALIZE_BYTES = 16 * 1024;
|
|
32
31
|
const EVENT_PREVIEW_MAX_BYTES = 512;
|
|
33
32
|
const EVENT_SEQ_TAIL_READ_BYTES = EVENT_LINE_MAX_BYTES + 1024;
|
|
33
|
+
// ─── Session Branch Auto-Commit ──────────────────────────────────────────────
|
|
34
|
+
// AC: @session-branch-worktree ac-commit-boundaries
|
|
35
|
+
// When sessionsDir is a git worktree (sessions.storage=branch), auto-commit at
|
|
36
|
+
// lifecycle boundaries (create, close, stale cleanup, compact).
|
|
37
|
+
// Event appends are NOT committed individually.
|
|
38
|
+
/**
|
|
39
|
+
* Check if sessionsDir is a git worktree (has a .git file, not directory).
|
|
40
|
+
* Cached per path to avoid repeated filesystem checks.
|
|
41
|
+
*/
|
|
42
|
+
const worktreeCache = new Map();
|
|
43
|
+
async function isSessionWorktree(sessionsDir) {
|
|
44
|
+
const cached = worktreeCache.get(sessionsDir);
|
|
45
|
+
if (cached !== undefined)
|
|
46
|
+
return cached;
|
|
47
|
+
try {
|
|
48
|
+
const gitPath = path.join(sessionsDir, ".git");
|
|
49
|
+
const stat = await fsPromises.stat(gitPath);
|
|
50
|
+
// Worktrees have a .git FILE pointing to the main repo
|
|
51
|
+
const isWorktree = stat.isFile();
|
|
52
|
+
worktreeCache.set(sessionsDir, isWorktree);
|
|
53
|
+
return isWorktree;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
worktreeCache.set(sessionsDir, false);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Auto-commit to session branch at lifecycle boundaries.
|
|
62
|
+
* No-op if sessionsDir is not a git worktree.
|
|
63
|
+
*/
|
|
64
|
+
async function commitAtLifecycleBoundary(sessionsDir, message) {
|
|
65
|
+
if (await isSessionWorktree(sessionsDir)) {
|
|
66
|
+
await sessionBranchAutoCommit(sessionsDir, message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
34
69
|
// ─── Path Helpers ────────────────────────────────────────────────────────────
|
|
70
|
+
// AC: @session-storage-path-resolution ac-resolver ac-path-helpers
|
|
71
|
+
// All path helpers accept sessionsDir (.kspec-sessions/ at project root),
|
|
72
|
+
// not sessionsDir (.kspec/). This decouples session storage from the shadow branch.
|
|
35
73
|
/**
|
|
36
|
-
* Get the sessions
|
|
74
|
+
* Get the sessions root directory.
|
|
75
|
+
* @deprecated Use ctx.sessionsDir directly. Kept for backward compatibility.
|
|
37
76
|
*/
|
|
38
|
-
export function getSessionsDir(
|
|
39
|
-
return
|
|
77
|
+
export function getSessionsDir(sessionsDir) {
|
|
78
|
+
return sessionsDir;
|
|
40
79
|
}
|
|
41
80
|
/**
|
|
42
81
|
* Get the path to a specific session's directory.
|
|
43
82
|
*/
|
|
44
|
-
export function getSessionDir(
|
|
45
|
-
return path.join(
|
|
83
|
+
export function getSessionDir(sessionsDir, sessionId) {
|
|
84
|
+
return path.join(sessionsDir, sessionId);
|
|
46
85
|
}
|
|
47
86
|
/**
|
|
48
87
|
* Get the path to a session's metadata file.
|
|
49
88
|
*/
|
|
50
|
-
export function getSessionMetadataPath(
|
|
51
|
-
return path.join(getSessionDir(
|
|
89
|
+
export function getSessionMetadataPath(sessionsDir, sessionId) {
|
|
90
|
+
return path.join(getSessionDir(sessionsDir, sessionId), METADATA_FILE);
|
|
52
91
|
}
|
|
53
92
|
/**
|
|
54
93
|
* Get the path to a session's events file.
|
|
55
94
|
*/
|
|
56
|
-
export function getSessionEventsPath(
|
|
57
|
-
return path.join(getSessionDir(
|
|
95
|
+
export function getSessionEventsPath(sessionsDir, sessionId) {
|
|
96
|
+
return path.join(getSessionDir(sessionsDir, sessionId), EVENTS_FILE);
|
|
58
97
|
}
|
|
59
98
|
/**
|
|
60
99
|
* Get the path to a session's context snapshot file for a given iteration.
|
|
61
100
|
*/
|
|
62
|
-
export function getSessionContextPath(
|
|
63
|
-
return path.join(getSessionDir(
|
|
101
|
+
export function getSessionContextPath(sessionsDir, sessionId, iteration) {
|
|
102
|
+
return path.join(getSessionDir(sessionsDir, sessionId), `context-iter-${iteration}.json`);
|
|
64
103
|
}
|
|
65
104
|
/**
|
|
66
105
|
* Get the path to a session's budget file.
|
|
67
106
|
* AC: @session-creation-and-env-injection ac-budget-local
|
|
68
107
|
*/
|
|
69
|
-
export function getSessionBudgetPath(
|
|
70
|
-
return path.join(getSessionDir(
|
|
108
|
+
export function getSessionBudgetPath(sessionsDir, sessionId) {
|
|
109
|
+
return path.join(getSessionDir(sessionsDir, sessionId), BUDGET_FILE);
|
|
71
110
|
}
|
|
72
111
|
/**
|
|
73
112
|
* Get the path to a session's blob directory.
|
|
74
113
|
*/
|
|
75
|
-
export function getSessionBlobDir(
|
|
76
|
-
return path.join(getSessionDir(
|
|
114
|
+
export function getSessionBlobDir(sessionsDir, sessionId) {
|
|
115
|
+
return path.join(getSessionDir(sessionsDir, sessionId), BLOBS_DIR);
|
|
77
116
|
}
|
|
78
117
|
// ─── Session CRUD ────────────────────────────────────────────────────────────
|
|
79
118
|
/**
|
|
80
119
|
* Create a new session with metadata.
|
|
81
120
|
*
|
|
82
|
-
* AC-1: Creates .kspec
|
|
121
|
+
* AC-1: Creates .kspec-sessions/{id}/ directory with session.yaml metadata file.
|
|
83
122
|
* AC-5: Metadata includes task_id (optional), agent_type, status, started_at, ended_at.
|
|
84
123
|
*
|
|
85
|
-
* @param
|
|
124
|
+
* @param sessionsDir - The .kspec directory path
|
|
86
125
|
* @param input - Session metadata input
|
|
87
126
|
* @returns The created session metadata
|
|
88
127
|
*/
|
|
89
|
-
export async function createSession(
|
|
90
|
-
const sessionDir = getSessionDir(
|
|
91
|
-
const metadataPath = getSessionMetadataPath(
|
|
128
|
+
export async function createSession(sessionsDir, input) {
|
|
129
|
+
const sessionDir = getSessionDir(sessionsDir, input.id);
|
|
130
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, input.id);
|
|
92
131
|
// Create session directory
|
|
93
132
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
94
133
|
// Build full metadata
|
|
@@ -111,17 +150,19 @@ export async function createSession(specDir, input) {
|
|
|
111
150
|
sortMapEntries: false,
|
|
112
151
|
});
|
|
113
152
|
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
153
|
+
// AC: @session-branch-worktree ac-commit-boundaries — commit on session create
|
|
154
|
+
await commitAtLifecycleBoundary(sessionsDir, `session: create (${input.id})`);
|
|
114
155
|
return validated;
|
|
115
156
|
}
|
|
116
157
|
/**
|
|
117
158
|
* Read session metadata.
|
|
118
159
|
*
|
|
119
|
-
* @param
|
|
160
|
+
* @param sessionsDir - The .kspec directory path
|
|
120
161
|
* @param sessionId - Session ID
|
|
121
162
|
* @returns Session metadata or null if not found
|
|
122
163
|
*/
|
|
123
|
-
export async function getSession(
|
|
124
|
-
const metadataPath = getSessionMetadataPath(
|
|
164
|
+
export async function getSession(sessionsDir, sessionId) {
|
|
165
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
125
166
|
try {
|
|
126
167
|
const content = await fsPromises.readFile(metadataPath, "utf-8");
|
|
127
168
|
const raw = YAML.parse(content);
|
|
@@ -142,13 +183,13 @@ export async function getSession(specDir, sessionId) {
|
|
|
142
183
|
*
|
|
143
184
|
* AC-6: Updates metadata with status and ended_at timestamp when session ends.
|
|
144
185
|
*
|
|
145
|
-
* @param
|
|
186
|
+
* @param sessionsDir - The .kspec directory path
|
|
146
187
|
* @param sessionId - Session ID
|
|
147
188
|
* @param status - New status
|
|
148
189
|
* @returns Updated metadata or null if session not found
|
|
149
190
|
*/
|
|
150
|
-
export async function updateSessionStatus(
|
|
151
|
-
const metadata = await getSession(
|
|
191
|
+
export async function updateSessionStatus(sessionsDir, sessionId, status) {
|
|
192
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
152
193
|
if (!metadata) {
|
|
153
194
|
return null;
|
|
154
195
|
}
|
|
@@ -158,7 +199,7 @@ export async function updateSessionStatus(specDir, sessionId, status) {
|
|
|
158
199
|
status,
|
|
159
200
|
ended_at: status !== "active" ? new Date().toISOString() : metadata.ended_at,
|
|
160
201
|
};
|
|
161
|
-
const metadataPath = getSessionMetadataPath(
|
|
202
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
162
203
|
const content = YAML.stringify(updated, {
|
|
163
204
|
indent: 2,
|
|
164
205
|
lineWidth: 100,
|
|
@@ -170,11 +211,10 @@ export async function updateSessionStatus(specDir, sessionId, status) {
|
|
|
170
211
|
/**
|
|
171
212
|
* List all sessions.
|
|
172
213
|
*
|
|
173
|
-
* @param
|
|
214
|
+
* @param sessionsDir - The .kspec directory path
|
|
174
215
|
* @returns Array of session IDs
|
|
175
216
|
*/
|
|
176
|
-
export async function listSessions(
|
|
177
|
-
const sessionsDir = getSessionsDir(specDir);
|
|
217
|
+
export async function listSessions(sessionsDir) {
|
|
178
218
|
try {
|
|
179
219
|
const entries = await fsPromises.readdir(sessionsDir, {
|
|
180
220
|
withFileTypes: true,
|
|
@@ -191,14 +231,14 @@ export async function listSessions(specDir) {
|
|
|
191
231
|
* Reads only session metadata (not events.jsonl) for performance.
|
|
192
232
|
* A session counts as "completed" if its status is "completed".
|
|
193
233
|
*
|
|
194
|
-
* @param
|
|
234
|
+
* @param sessionsDir - The .kspec directory path
|
|
195
235
|
* @returns Map of agent_id to completed session count
|
|
196
236
|
*/
|
|
197
|
-
export async function getCompletedSessionCountsByAgent(
|
|
198
|
-
const sessionIds = await listSessions(
|
|
237
|
+
export async function getCompletedSessionCountsByAgent(sessionsDir) {
|
|
238
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
199
239
|
const counts = {};
|
|
200
240
|
// Read metadata in parallel for performance
|
|
201
|
-
const metadataResults = await Promise.all(sessionIds.map((id) => getSession(
|
|
241
|
+
const metadataResults = await Promise.all(sessionIds.map((id) => getSession(sessionsDir, id)));
|
|
202
242
|
for (const metadata of metadataResults) {
|
|
203
243
|
if (!metadata)
|
|
204
244
|
continue;
|
|
@@ -212,8 +252,8 @@ export async function getCompletedSessionCountsByAgent(specDir) {
|
|
|
212
252
|
/**
|
|
213
253
|
* Check if a session exists.
|
|
214
254
|
*/
|
|
215
|
-
export async function sessionExists(
|
|
216
|
-
const metadataPath = getSessionMetadataPath(
|
|
255
|
+
export async function sessionExists(sessionsDir, sessionId) {
|
|
256
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
217
257
|
try {
|
|
218
258
|
await fsPromises.access(metadataPath);
|
|
219
259
|
return true;
|
|
@@ -231,13 +271,13 @@ export async function sessionExists(specDir, sessionId) {
|
|
|
231
271
|
*
|
|
232
272
|
* AC: @session-end-loop-signal ac-signal
|
|
233
273
|
*
|
|
234
|
-
* @param
|
|
274
|
+
* @param sessionsDir - The .kspec directory path
|
|
235
275
|
* @param sessionId - Session ID
|
|
236
276
|
* @param reason - Optional reason for ending the loop
|
|
237
277
|
* @returns Updated metadata or null if session not found
|
|
238
278
|
*/
|
|
239
|
-
export async function requestEndLoop(
|
|
240
|
-
const metadata = await getSession(
|
|
279
|
+
export async function requestEndLoop(sessionsDir, sessionId, reason) {
|
|
280
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
241
281
|
if (!metadata) {
|
|
242
282
|
return null;
|
|
243
283
|
}
|
|
@@ -246,7 +286,7 @@ export async function requestEndLoop(specDir, sessionId, reason) {
|
|
|
246
286
|
end_requested: true,
|
|
247
287
|
end_reason: reason,
|
|
248
288
|
};
|
|
249
|
-
const metadataPath = getSessionMetadataPath(
|
|
289
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
250
290
|
const content = YAML.stringify(updated, {
|
|
251
291
|
indent: 2,
|
|
252
292
|
lineWidth: 100,
|
|
@@ -264,12 +304,12 @@ export async function requestEndLoop(specDir, sessionId, reason) {
|
|
|
264
304
|
*
|
|
265
305
|
* AC: @session-end-loop-signal ac-detect
|
|
266
306
|
*
|
|
267
|
-
* @param
|
|
307
|
+
* @param sessionsDir - The .kspec directory path
|
|
268
308
|
* @param sessionId - Session ID
|
|
269
309
|
* @returns Object with requested flag and optional reason, or null if session not found
|
|
270
310
|
*/
|
|
271
|
-
export async function isEndLoopRequested(
|
|
272
|
-
const metadata = await getSession(
|
|
311
|
+
export async function isEndLoopRequested(sessionsDir, sessionId) {
|
|
312
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
273
313
|
if (!metadata) {
|
|
274
314
|
return null;
|
|
275
315
|
}
|
|
@@ -287,30 +327,42 @@ export async function isEndLoopRequested(specDir, sessionId) {
|
|
|
287
327
|
* AC: @session-end-loop-signal ac-session-close-signal
|
|
288
328
|
* AC: @session-end-loop-signal ac-session-close-error
|
|
289
329
|
*
|
|
290
|
-
* @param
|
|
330
|
+
* @param sessionsDir - The .kspec directory path
|
|
291
331
|
* @param sessionId - Session ID
|
|
292
332
|
* @param status - New status (completed or abandoned)
|
|
293
333
|
* @param reason - Reason for closing
|
|
294
334
|
* @returns Updated metadata or null if session not found
|
|
295
335
|
*/
|
|
296
|
-
export async function closeSession(
|
|
297
|
-
const metadata = await getSession(
|
|
336
|
+
export async function closeSession(sessionsDir, sessionId, status, reason) {
|
|
337
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
298
338
|
if (!metadata) {
|
|
299
339
|
return null;
|
|
300
340
|
}
|
|
341
|
+
// AC: @session-summary-cache ac-persist-on-close — compute stats from events.jsonl
|
|
342
|
+
// and persist in session metadata so list endpoints never need to scan event data.
|
|
343
|
+
const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
|
|
344
|
+
countEventLines(sessionsDir, sessionId),
|
|
345
|
+
countIterations(sessionsDir, sessionId),
|
|
346
|
+
countTaskCompletions(sessionsDir, sessionId),
|
|
347
|
+
]);
|
|
301
348
|
const updated = {
|
|
302
349
|
...metadata,
|
|
303
350
|
status,
|
|
304
351
|
ended_at: new Date().toISOString(),
|
|
305
352
|
close_reason: reason,
|
|
353
|
+
event_count: eventCount,
|
|
354
|
+
iteration_count: iterationCount,
|
|
355
|
+
tasks_completed: tasksCompleted,
|
|
306
356
|
};
|
|
307
|
-
const metadataPath = getSessionMetadataPath(
|
|
357
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
308
358
|
const content = YAML.stringify(updated, {
|
|
309
359
|
indent: 2,
|
|
310
360
|
lineWidth: 100,
|
|
311
361
|
sortMapEntries: false,
|
|
312
362
|
});
|
|
313
363
|
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
364
|
+
// AC: @session-branch-worktree ac-commit-boundaries — commit on session close
|
|
365
|
+
await commitAtLifecycleBoundary(sessionsDir, `session: close ${status} (${sessionId})`);
|
|
314
366
|
return updated;
|
|
315
367
|
}
|
|
316
368
|
function isRecord(value) {
|
|
@@ -384,7 +436,7 @@ function shouldExternalizeField(pathSegments, value) {
|
|
|
384
436
|
}
|
|
385
437
|
return false;
|
|
386
438
|
}
|
|
387
|
-
async function createBlobPointer(
|
|
439
|
+
async function createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context) {
|
|
388
440
|
const content = stringifyPayload(value);
|
|
389
441
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
390
442
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
@@ -399,7 +451,7 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
|
|
|
399
451
|
await fsPromises.mkdir(context.blobDir, { recursive: true });
|
|
400
452
|
context.ensuredDir = true;
|
|
401
453
|
}
|
|
402
|
-
const absolutePath = path.join(getSessionDir(
|
|
454
|
+
const absolutePath = path.join(getSessionDir(sessionsDir, sessionId), relativePath);
|
|
403
455
|
await fsPromises.writeFile(absolutePath, content, "utf-8");
|
|
404
456
|
}
|
|
405
457
|
context.createdBlobs = (context.createdBlobs ?? 0) + 1;
|
|
@@ -412,15 +464,15 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
|
|
|
412
464
|
preview: toPreview(content),
|
|
413
465
|
};
|
|
414
466
|
}
|
|
415
|
-
async function externalizeOversizedPayloads(
|
|
467
|
+
async function externalizeOversizedPayloads(sessionsDir, sessionId, seq, value, pathSegments, context) {
|
|
416
468
|
if (isSessionBlobPointer(value)) {
|
|
417
469
|
return value;
|
|
418
470
|
}
|
|
419
471
|
if (shouldExternalizeField(pathSegments, value)) {
|
|
420
|
-
return createBlobPointer(
|
|
472
|
+
return createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context);
|
|
421
473
|
}
|
|
422
474
|
if (Array.isArray(value)) {
|
|
423
|
-
return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(
|
|
475
|
+
return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(sessionsDir, sessionId, seq, entry, [
|
|
424
476
|
...pathSegments,
|
|
425
477
|
String(idx),
|
|
426
478
|
], context)));
|
|
@@ -428,14 +480,14 @@ async function externalizeOversizedPayloads(specDir, sessionId, seq, value, path
|
|
|
428
480
|
if (isRecord(value)) {
|
|
429
481
|
const next = {};
|
|
430
482
|
for (const [key, child] of Object.entries(value)) {
|
|
431
|
-
next[key] = await externalizeOversizedPayloads(
|
|
483
|
+
next[key] = await externalizeOversizedPayloads(sessionsDir, sessionId, seq, child, [...pathSegments, key], context);
|
|
432
484
|
}
|
|
433
485
|
return next;
|
|
434
486
|
}
|
|
435
487
|
return value;
|
|
436
488
|
}
|
|
437
|
-
function resolveBlobAbsolutePath(
|
|
438
|
-
const sessionDir = path.resolve(getSessionDir(
|
|
489
|
+
function resolveBlobAbsolutePath(sessionsDir, sessionId, relativePath) {
|
|
490
|
+
const sessionDir = path.resolve(getSessionDir(sessionsDir, sessionId));
|
|
439
491
|
const absolutePath = path.resolve(sessionDir, relativePath);
|
|
440
492
|
if (absolutePath === sessionDir ||
|
|
441
493
|
absolutePath.startsWith(`${sessionDir}${path.sep}`)) {
|
|
@@ -443,8 +495,8 @@ function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
|
|
|
443
495
|
}
|
|
444
496
|
return null;
|
|
445
497
|
}
|
|
446
|
-
async function resolveBlobPointer(
|
|
447
|
-
const absolutePath = resolveBlobAbsolutePath(
|
|
498
|
+
async function resolveBlobPointer(sessionsDir, sessionId, pointer) {
|
|
499
|
+
const absolutePath = resolveBlobAbsolutePath(sessionsDir, sessionId, pointer.path);
|
|
448
500
|
if (!absolutePath) {
|
|
449
501
|
return { ...pointer, content: pointer.preview };
|
|
450
502
|
}
|
|
@@ -462,17 +514,17 @@ async function resolveBlobPointer(specDir, sessionId, pointer) {
|
|
|
462
514
|
* Default flows keep compact pointer objects (preview-only). This helper powers
|
|
463
515
|
* explicit on-demand blob resolution in session log commands.
|
|
464
516
|
*/
|
|
465
|
-
export async function resolveSessionBlobPointers(
|
|
517
|
+
export async function resolveSessionBlobPointers(sessionsDir, sessionId, value) {
|
|
466
518
|
if (isSessionBlobPointer(value)) {
|
|
467
|
-
return resolveBlobPointer(
|
|
519
|
+
return resolveBlobPointer(sessionsDir, sessionId, value);
|
|
468
520
|
}
|
|
469
521
|
if (Array.isArray(value)) {
|
|
470
|
-
return Promise.all(value.map((entry) => resolveSessionBlobPointers(
|
|
522
|
+
return Promise.all(value.map((entry) => resolveSessionBlobPointers(sessionsDir, sessionId, entry)));
|
|
471
523
|
}
|
|
472
524
|
if (isRecord(value)) {
|
|
473
525
|
const next = {};
|
|
474
526
|
for (const [key, child] of Object.entries(value)) {
|
|
475
|
-
next[key] = await resolveSessionBlobPointers(
|
|
527
|
+
next[key] = await resolveSessionBlobPointers(sessionsDir, sessionId, child);
|
|
476
528
|
}
|
|
477
529
|
return next;
|
|
478
530
|
}
|
|
@@ -506,8 +558,8 @@ function extractLastEventSeq(content) {
|
|
|
506
558
|
* Reads a bounded tail slice for O(1) seq lookup; falls back to full scan only
|
|
507
559
|
* if the tail slice cannot be parsed (for example, partial line boundary).
|
|
508
560
|
*/
|
|
509
|
-
async function getNextEventSeq(
|
|
510
|
-
const eventsPath = getSessionEventsPath(
|
|
561
|
+
async function getNextEventSeq(sessionsDir, sessionId) {
|
|
562
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
511
563
|
let fileHandle = null;
|
|
512
564
|
try {
|
|
513
565
|
fileHandle = await fsPromises.open(eventsPath, "r");
|
|
@@ -561,17 +613,17 @@ async function getNextEventSeq(specDir, sessionId) {
|
|
|
561
613
|
* (single-process, sequential event logging). If concurrent access is needed
|
|
562
614
|
* in the future, consider file locking or an in-memory counter per session.
|
|
563
615
|
*
|
|
564
|
-
* @param
|
|
616
|
+
* @param sessionsDir - The .kspec directory path
|
|
565
617
|
* @param input - Event input (ts and seq are auto-assigned if not provided)
|
|
566
618
|
* @returns The appended event with auto-assigned fields
|
|
567
619
|
*/
|
|
568
|
-
export async function appendEvent(
|
|
569
|
-
const sessionDir = getSessionDir(
|
|
570
|
-
const eventsPath = getSessionEventsPath(
|
|
620
|
+
export async function appendEvent(sessionsDir, input) {
|
|
621
|
+
const sessionDir = getSessionDir(sessionsDir, input.session_id);
|
|
622
|
+
const eventsPath = getSessionEventsPath(sessionsDir, input.session_id);
|
|
571
623
|
// Ensure session directory exists (lazy creation)
|
|
572
624
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
573
625
|
// Derive next sequence number from the last stored event.
|
|
574
|
-
const seq = input.seq ?? (await getNextEventSeq(
|
|
626
|
+
const seq = input.seq ?? (await getNextEventSeq(sessionsDir, input.session_id));
|
|
575
627
|
// Build full event
|
|
576
628
|
const event = {
|
|
577
629
|
ts: input.ts ?? Date.now(),
|
|
@@ -583,8 +635,8 @@ export async function appendEvent(specDir, input) {
|
|
|
583
635
|
};
|
|
584
636
|
// AC: @session-events ac-8, ac-9 - Externalize oversized payload fields
|
|
585
637
|
// before writing to events.jsonl.
|
|
586
|
-
const externalizedData = await externalizeOversizedPayloads(
|
|
587
|
-
blobDir: getSessionBlobDir(
|
|
638
|
+
const externalizedData = await externalizeOversizedPayloads(sessionsDir, input.session_id, seq, event.data, [], {
|
|
639
|
+
blobDir: getSessionBlobDir(sessionsDir, input.session_id),
|
|
588
640
|
ensuredDir: false,
|
|
589
641
|
});
|
|
590
642
|
const eventWithGuardrails = {
|
|
@@ -598,10 +650,10 @@ export async function appendEvent(specDir, input) {
|
|
|
598
650
|
let line = JSON.stringify(validated);
|
|
599
651
|
if (Buffer.byteLength(line, "utf-8") > EVENT_LINE_MAX_BYTES) {
|
|
600
652
|
const blobContext = {
|
|
601
|
-
blobDir: getSessionBlobDir(
|
|
653
|
+
blobDir: getSessionBlobDir(sessionsDir, input.session_id),
|
|
602
654
|
ensuredDir: false,
|
|
603
655
|
};
|
|
604
|
-
const fullDataPointer = await createBlobPointer(
|
|
656
|
+
const fullDataPointer = await createBlobPointer(sessionsDir, input.session_id, seq, [], validated.data, blobContext);
|
|
605
657
|
validated = SessionEventSchema.parse({
|
|
606
658
|
...validated,
|
|
607
659
|
data: fullDataPointer,
|
|
@@ -623,10 +675,10 @@ export async function appendEvent(specDir, input) {
|
|
|
623
675
|
* Writes are atomic (temp-file then rename). When dryRun is enabled, no files
|
|
624
676
|
* are modified and no blob files are written.
|
|
625
677
|
*/
|
|
626
|
-
export async function compactSessionEvents(
|
|
678
|
+
export async function compactSessionEvents(sessionsDir, sessionId, options = {}) {
|
|
627
679
|
const dryRun = options.dryRun === true;
|
|
628
680
|
const renameFn = options.renameFn ?? fsPromises.rename;
|
|
629
|
-
const eventsPath = getSessionEventsPath(
|
|
681
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
630
682
|
let content;
|
|
631
683
|
try {
|
|
632
684
|
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
@@ -663,7 +715,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
663
715
|
};
|
|
664
716
|
}
|
|
665
717
|
const blobContext = {
|
|
666
|
-
blobDir: getSessionBlobDir(
|
|
718
|
+
blobDir: getSessionBlobDir(sessionsDir, sessionId),
|
|
667
719
|
ensuredDir: false,
|
|
668
720
|
dryRun,
|
|
669
721
|
createdBlobs: 0,
|
|
@@ -680,14 +732,14 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
680
732
|
throw new Error(`Invalid JSON in events log at line ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
681
733
|
}
|
|
682
734
|
const event = SessionEventSchema.parse(parsed);
|
|
683
|
-
const externalizedData = await externalizeOversizedPayloads(
|
|
735
|
+
const externalizedData = await externalizeOversizedPayloads(sessionsDir, sessionId, event.seq, event.data, [], blobContext);
|
|
684
736
|
let validated = SessionEventSchema.parse({
|
|
685
737
|
...event,
|
|
686
738
|
data: externalizedData,
|
|
687
739
|
});
|
|
688
740
|
let compactedLine = JSON.stringify(validated);
|
|
689
741
|
if (Buffer.byteLength(compactedLine, "utf-8") > EVENT_LINE_MAX_BYTES) {
|
|
690
|
-
const fullDataPointer = await createBlobPointer(
|
|
742
|
+
const fullDataPointer = await createBlobPointer(sessionsDir, sessionId, event.seq, [], validated.data, blobContext);
|
|
691
743
|
validated = SessionEventSchema.parse({
|
|
692
744
|
...validated,
|
|
693
745
|
data: fullDataPointer,
|
|
@@ -712,7 +764,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
712
764
|
};
|
|
713
765
|
}
|
|
714
766
|
if (!dryRun) {
|
|
715
|
-
const sessionDir = getSessionDir(
|
|
767
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
716
768
|
const tmpPath = path.join(sessionDir, `.${EVENTS_FILE}.${process.pid}.${Date.now()}.tmp`);
|
|
717
769
|
try {
|
|
718
770
|
await fsPromises.writeFile(tmpPath, compactedContent, "utf-8");
|
|
@@ -723,6 +775,10 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
723
775
|
throw err;
|
|
724
776
|
}
|
|
725
777
|
}
|
|
778
|
+
// AC: @session-branch-worktree ac-commit-boundaries — commit on compact
|
|
779
|
+
if (!dryRun) {
|
|
780
|
+
await commitAtLifecycleBoundary(sessionsDir, `session: compact (${sessionId})`);
|
|
781
|
+
}
|
|
726
782
|
return {
|
|
727
783
|
events_processed: sourceLines.length,
|
|
728
784
|
blobs_created: blobContext.createdBlobs ?? 0,
|
|
@@ -739,12 +795,12 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
739
795
|
*
|
|
740
796
|
* AC-4: Returns all events in sequence order.
|
|
741
797
|
*
|
|
742
|
-
* @param
|
|
798
|
+
* @param sessionsDir - The .kspec directory path
|
|
743
799
|
* @param sessionId - Session ID
|
|
744
800
|
* @returns Array of events sorted by sequence number
|
|
745
801
|
*/
|
|
746
|
-
export async function readEvents(
|
|
747
|
-
const eventsPath = getSessionEventsPath(
|
|
802
|
+
export async function readEvents(sessionsDir, sessionId) {
|
|
803
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
748
804
|
try {
|
|
749
805
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
750
806
|
const lines = content
|
|
@@ -769,6 +825,45 @@ export async function readEvents(specDir, sessionId) {
|
|
|
769
825
|
return [];
|
|
770
826
|
}
|
|
771
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Read a single event by sequence number from a session's event log.
|
|
830
|
+
*
|
|
831
|
+
* Scans events.jsonl line-by-line and stops as soon as the matching seq is
|
|
832
|
+
* found, avoiding full parsing of all events for large sessions.
|
|
833
|
+
*
|
|
834
|
+
* AC: @session-event-detail-endpoint ac-single-event-fetch
|
|
835
|
+
*
|
|
836
|
+
* @param sessionsDir - The sessions directory path
|
|
837
|
+
* @param sessionId - Session ID
|
|
838
|
+
* @param seq - Sequence number to find
|
|
839
|
+
* @returns The matching event, or null if not found
|
|
840
|
+
*/
|
|
841
|
+
export async function readEventBySeq(sessionsDir, sessionId, seq) {
|
|
842
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
843
|
+
let content;
|
|
844
|
+
try {
|
|
845
|
+
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
const lines = content.split("\n");
|
|
851
|
+
for (const line of lines) {
|
|
852
|
+
const trimmed = line.trim();
|
|
853
|
+
if (trimmed.length === 0)
|
|
854
|
+
continue;
|
|
855
|
+
try {
|
|
856
|
+
const raw = JSON.parse(trimmed);
|
|
857
|
+
if (isRecord(raw) && raw.seq === seq) {
|
|
858
|
+
return SessionEventSchema.parse(raw);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Skip invalid lines
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
772
867
|
/**
|
|
773
868
|
* Deduplicate phased tool_call events.
|
|
774
869
|
*
|
|
@@ -818,14 +913,14 @@ export function deduplicatePhasedToolCalls(events) {
|
|
|
818
913
|
/**
|
|
819
914
|
* Read events within a time range.
|
|
820
915
|
*
|
|
821
|
-
* @param
|
|
916
|
+
* @param sessionsDir - The .kspec directory path
|
|
822
917
|
* @param sessionId - Session ID
|
|
823
918
|
* @param since - Start timestamp (inclusive)
|
|
824
919
|
* @param until - End timestamp (inclusive)
|
|
825
920
|
* @returns Array of events within the time range
|
|
826
921
|
*/
|
|
827
|
-
export async function readEventsSince(
|
|
828
|
-
const events = await readEvents(
|
|
922
|
+
export async function readEventsSince(sessionsDir, sessionId, since, until) {
|
|
923
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
829
924
|
return events.filter((e) => {
|
|
830
925
|
if (e.ts < since)
|
|
831
926
|
return false;
|
|
@@ -837,12 +932,12 @@ export async function readEventsSince(specDir, sessionId, since, until) {
|
|
|
837
932
|
/**
|
|
838
933
|
* Get the last event in a session.
|
|
839
934
|
*
|
|
840
|
-
* @param
|
|
935
|
+
* @param sessionsDir - The .kspec directory path
|
|
841
936
|
* @param sessionId - Session ID
|
|
842
937
|
* @returns The last event or null if no events
|
|
843
938
|
*/
|
|
844
|
-
export async function getLastEvent(
|
|
845
|
-
const events = await readEvents(
|
|
939
|
+
export async function getLastEvent(sessionsDir, sessionId) {
|
|
940
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
846
941
|
if (events.length === 0) {
|
|
847
942
|
return null;
|
|
848
943
|
}
|
|
@@ -930,8 +1025,8 @@ export function resolveStaleSessionCriteria(input) {
|
|
|
930
1025
|
* Unlike readEvents(), this is strict: corrupt events are surfaced as failures
|
|
931
1026
|
* so stale auto-close can skip unsafe sessions.
|
|
932
1027
|
*/
|
|
933
|
-
export async function getSessionActivityForStaleCheck(
|
|
934
|
-
const metadata = await getSession(
|
|
1028
|
+
export async function getSessionActivityForStaleCheck(sessionsDir, sessionId) {
|
|
1029
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
935
1030
|
if (!metadata) {
|
|
936
1031
|
return {
|
|
937
1032
|
ok: false,
|
|
@@ -947,7 +1042,7 @@ export async function getSessionActivityForStaleCheck(specDir, sessionId) {
|
|
|
947
1042
|
detail: `Session ${sessionId} has invalid started_at timestamp`,
|
|
948
1043
|
};
|
|
949
1044
|
}
|
|
950
|
-
const eventsPath = getSessionEventsPath(
|
|
1045
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
951
1046
|
let content;
|
|
952
1047
|
try {
|
|
953
1048
|
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
@@ -1043,21 +1138,21 @@ export function evaluateStaleSession(startedAt, activity, criteria, nowMs = Date
|
|
|
1043
1138
|
eligible,
|
|
1044
1139
|
};
|
|
1045
1140
|
}
|
|
1046
|
-
export async function selectStaleActiveSessions(
|
|
1141
|
+
export async function selectStaleActiveSessions(sessionsDir, criteriaInput = {}, nowMs = Date.now()) {
|
|
1047
1142
|
const criteriaResolved = resolveStaleSessionCriteria(criteriaInput);
|
|
1048
1143
|
if (!criteriaResolved.ok) {
|
|
1049
1144
|
throw new Error(`${criteriaResolved.message}. ${criteriaResolved.guidance}`);
|
|
1050
1145
|
}
|
|
1051
1146
|
const criteria = criteriaResolved.criteria;
|
|
1052
|
-
const sessionIds = await listSessions(
|
|
1147
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
1053
1148
|
const evaluations = [];
|
|
1054
1149
|
const candidates = [];
|
|
1055
1150
|
const skipped = [];
|
|
1056
1151
|
for (const sessionId of sessionIds) {
|
|
1057
|
-
const metadata = await getSession(
|
|
1152
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1058
1153
|
if (!metadata || metadata.status !== "active")
|
|
1059
1154
|
continue;
|
|
1060
|
-
const activityResult = await getSessionActivityForStaleCheck(
|
|
1155
|
+
const activityResult = await getSessionActivityForStaleCheck(sessionsDir, sessionId);
|
|
1061
1156
|
if (!activityResult.ok) {
|
|
1062
1157
|
skipped.push({
|
|
1063
1158
|
sessionId,
|
|
@@ -1105,9 +1200,9 @@ export function buildAutoAbandonedCloseReason(criteria, evaluation) {
|
|
|
1105
1200
|
* Apply abandoned metadata to stale session candidates.
|
|
1106
1201
|
*
|
|
1107
1202
|
* All updates in a single invocation share one ended_at timestamp, which lets
|
|
1108
|
-
* the caller persist and commit the batch atomically
|
|
1203
|
+
* the caller persist and commit the batch atomically.
|
|
1109
1204
|
*/
|
|
1110
|
-
export async function applyAutoAbandonMetadata(
|
|
1205
|
+
export async function applyAutoAbandonMetadata(sessionsDir, selection, options) {
|
|
1111
1206
|
const dryRun = options?.dryRun === true;
|
|
1112
1207
|
const endedAt = new Date(options?.nowMs ?? Date.now()).toISOString();
|
|
1113
1208
|
const updates = [];
|
|
@@ -1121,7 +1216,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
|
1121
1216
|
});
|
|
1122
1217
|
if (dryRun)
|
|
1123
1218
|
continue;
|
|
1124
|
-
const metadata = await getSession(
|
|
1219
|
+
const metadata = await getSession(sessionsDir, candidate.sessionId);
|
|
1125
1220
|
if (!metadata)
|
|
1126
1221
|
continue;
|
|
1127
1222
|
const updated = {
|
|
@@ -1130,7 +1225,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
|
1130
1225
|
ended_at: endedAt,
|
|
1131
1226
|
close_reason: closeReason,
|
|
1132
1227
|
};
|
|
1133
|
-
const metadataPath = getSessionMetadataPath(
|
|
1228
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, candidate.sessionId);
|
|
1134
1229
|
const content = YAML.stringify(updated, {
|
|
1135
1230
|
indent: 2,
|
|
1136
1231
|
lineWidth: 100,
|
|
@@ -1138,15 +1233,14 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
|
1138
1233
|
});
|
|
1139
1234
|
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
1140
1235
|
}
|
|
1141
|
-
|
|
1142
|
-
if (!dryRun && updates.length > 0
|
|
1143
|
-
|
|
1236
|
+
// AC: @session-branch-worktree ac-commit-boundaries — commit on stale cleanup
|
|
1237
|
+
if (!dryRun && updates.length > 0) {
|
|
1238
|
+
await commitAtLifecycleBoundary(sessionsDir, `session: stale cleanup (${updates.length} abandoned)`);
|
|
1144
1239
|
}
|
|
1145
1240
|
return {
|
|
1146
1241
|
dryRun,
|
|
1147
1242
|
updatedCount: updates.length,
|
|
1148
1243
|
updates,
|
|
1149
|
-
shadowCommitted,
|
|
1150
1244
|
};
|
|
1151
1245
|
}
|
|
1152
1246
|
// ─── Session Log Summaries ───────────────────────────────────────────────────
|
|
@@ -1167,8 +1261,8 @@ function resolveSessionType(metadata) {
|
|
|
1167
1261
|
* Count lines in events.jsonl without parsing JSON.
|
|
1168
1262
|
* Much faster than readEvents() for large files.
|
|
1169
1263
|
*/
|
|
1170
|
-
async function countEventLines(
|
|
1171
|
-
const eventsPath = getSessionEventsPath(
|
|
1264
|
+
async function countEventLines(sessionsDir, sessionId) {
|
|
1265
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
1172
1266
|
try {
|
|
1173
1267
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1174
1268
|
if (!content.trim())
|
|
@@ -1182,8 +1276,8 @@ async function countEventLines(specDir, sessionId) {
|
|
|
1182
1276
|
/**
|
|
1183
1277
|
* Count context-iter-*.json files for a session (iteration count).
|
|
1184
1278
|
*/
|
|
1185
|
-
async function countIterations(
|
|
1186
|
-
const sessionDir = getSessionDir(
|
|
1279
|
+
async function countIterations(sessionsDir, sessionId) {
|
|
1280
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
1187
1281
|
try {
|
|
1188
1282
|
const entries = await fsPromises.readdir(sessionDir);
|
|
1189
1283
|
return entries.filter((e) => e.startsWith("context-iter-") && e.endsWith(".json")).length;
|
|
@@ -1205,8 +1299,8 @@ async function countIterations(specDir, sessionId) {
|
|
|
1205
1299
|
*
|
|
1206
1300
|
* We use a fast substring check before JSON parsing for performance.
|
|
1207
1301
|
*/
|
|
1208
|
-
async function countTaskCompletions(
|
|
1209
|
-
const eventsPath = getSessionEventsPath(
|
|
1302
|
+
async function countTaskCompletions(sessionsDir, sessionId) {
|
|
1303
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
1210
1304
|
try {
|
|
1211
1305
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1212
1306
|
if (!content.trim())
|
|
@@ -1245,18 +1339,18 @@ async function countTaskCompletions(specDir, sessionId) {
|
|
|
1245
1339
|
*
|
|
1246
1340
|
* Gathers metadata and computes metrics lazily (only parses what's needed).
|
|
1247
1341
|
*
|
|
1248
|
-
* @param
|
|
1342
|
+
* @param sessionsDir - The .kspec directory path
|
|
1249
1343
|
* @param sessionId - Session ID
|
|
1250
1344
|
* @returns Session summary or null if session doesn't exist
|
|
1251
1345
|
*/
|
|
1252
|
-
export async function getSessionLogSummary(
|
|
1253
|
-
const metadata = await getSession(
|
|
1346
|
+
export async function getSessionLogSummary(sessionsDir, sessionId) {
|
|
1347
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1254
1348
|
if (!metadata)
|
|
1255
1349
|
return null;
|
|
1256
1350
|
const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
|
|
1257
|
-
countEventLines(
|
|
1258
|
-
countIterationsBoundaryAware(
|
|
1259
|
-
countTaskCompletions(
|
|
1351
|
+
countEventLines(sessionsDir, sessionId),
|
|
1352
|
+
countIterationsBoundaryAware(sessionsDir, sessionId),
|
|
1353
|
+
countTaskCompletions(sessionsDir, sessionId),
|
|
1260
1354
|
]);
|
|
1261
1355
|
const startMs = new Date(metadata.started_at).getTime();
|
|
1262
1356
|
const endMs = metadata.ended_at
|
|
@@ -1267,6 +1361,7 @@ export async function getSessionLogSummary(specDir, sessionId) {
|
|
|
1267
1361
|
id: metadata.id,
|
|
1268
1362
|
status: metadata.status,
|
|
1269
1363
|
agent_type: metadata.agent_type,
|
|
1364
|
+
agent_id: metadata.agent_id,
|
|
1270
1365
|
session_type: resolveSessionType(metadata),
|
|
1271
1366
|
trigger: metadata.trigger,
|
|
1272
1367
|
task_id: metadata.task_id,
|
|
@@ -1278,15 +1373,48 @@ export async function getSessionLogSummary(specDir, sessionId) {
|
|
|
1278
1373
|
tasks_completed: tasksCompleted,
|
|
1279
1374
|
};
|
|
1280
1375
|
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Get session metadata only — reads session.yaml without touching events.jsonl.
|
|
1378
|
+
*
|
|
1379
|
+
* AC: @session-list-pagination-api ac-metadata-only — List endpoint reads only
|
|
1380
|
+
* session.yaml metadata.
|
|
1381
|
+
* AC: @session-summary-cache ac-persist-on-close — Closed sessions have stats
|
|
1382
|
+
* persisted in metadata; read them instead of hardcoding 0.
|
|
1383
|
+
*/
|
|
1384
|
+
export async function getSessionMetadataOnly(sessionsDir, sessionId) {
|
|
1385
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1386
|
+
if (!metadata)
|
|
1387
|
+
return null;
|
|
1388
|
+
const startMs = new Date(metadata.started_at).getTime();
|
|
1389
|
+
const endMs = metadata.ended_at
|
|
1390
|
+
? new Date(metadata.ended_at).getTime()
|
|
1391
|
+
: Date.now();
|
|
1392
|
+
const durationMs = endMs - startMs;
|
|
1393
|
+
return {
|
|
1394
|
+
id: metadata.id,
|
|
1395
|
+
status: metadata.status,
|
|
1396
|
+
agent_type: metadata.agent_type,
|
|
1397
|
+
agent_id: metadata.agent_id,
|
|
1398
|
+
session_type: resolveSessionType(metadata),
|
|
1399
|
+
trigger: metadata.trigger,
|
|
1400
|
+
task_id: metadata.task_id,
|
|
1401
|
+
started_at: metadata.started_at,
|
|
1402
|
+
ended_at: metadata.ended_at,
|
|
1403
|
+
duration_ms: durationMs,
|
|
1404
|
+
event_count: metadata.event_count ?? 0,
|
|
1405
|
+
iteration_count: metadata.iteration_count ?? 0,
|
|
1406
|
+
tasks_completed: metadata.tasks_completed ?? 0,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1281
1409
|
/**
|
|
1282
1410
|
* Get summaries for all sessions.
|
|
1283
1411
|
*
|
|
1284
|
-
* @param
|
|
1412
|
+
* @param sessionsDir - The .kspec directory path
|
|
1285
1413
|
* @returns Array of session summaries
|
|
1286
1414
|
*/
|
|
1287
|
-
export async function getAllSessionLogSummaries(
|
|
1288
|
-
const sessionIds = await listSessions(
|
|
1289
|
-
const summaries = await Promise.all(sessionIds.map((id) => getSessionLogSummary(
|
|
1415
|
+
export async function getAllSessionLogSummaries(sessionsDir) {
|
|
1416
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
1417
|
+
const summaries = await Promise.all(sessionIds.map((id) => getSessionLogSummary(sessionsDir, id)));
|
|
1290
1418
|
return summaries.filter((s) => s !== null);
|
|
1291
1419
|
}
|
|
1292
1420
|
// ─── Context Snapshots ───────────────────────────────────────────────────────
|
|
@@ -1296,14 +1424,14 @@ export async function getAllSessionLogSummaries(specDir) {
|
|
|
1296
1424
|
* This creates an audit trail of what context the agent saw at each iteration,
|
|
1297
1425
|
* useful for debugging and reviewing agent behavior.
|
|
1298
1426
|
*
|
|
1299
|
-
* @param
|
|
1427
|
+
* @param sessionsDir - The .kspec directory path
|
|
1300
1428
|
* @param sessionId - Session ID
|
|
1301
1429
|
* @param iteration - Iteration number
|
|
1302
1430
|
* @param context - The session context data
|
|
1303
1431
|
*/
|
|
1304
|
-
export async function saveSessionContext(
|
|
1305
|
-
const sessionDir = getSessionDir(
|
|
1306
|
-
const contextPath = getSessionContextPath(
|
|
1432
|
+
export async function saveSessionContext(sessionsDir, sessionId, iteration, context) {
|
|
1433
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
1434
|
+
const contextPath = getSessionContextPath(sessionsDir, sessionId, iteration);
|
|
1307
1435
|
// Ensure session directory exists
|
|
1308
1436
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
1309
1437
|
// Write context snapshot as pretty JSON
|
|
@@ -1313,13 +1441,13 @@ export async function saveSessionContext(specDir, sessionId, iteration, context)
|
|
|
1313
1441
|
/**
|
|
1314
1442
|
* Read session context snapshot for a given iteration.
|
|
1315
1443
|
*
|
|
1316
|
-
* @param
|
|
1444
|
+
* @param sessionsDir - The .kspec directory path
|
|
1317
1445
|
* @param sessionId - Session ID
|
|
1318
1446
|
* @param iteration - Iteration number
|
|
1319
1447
|
* @returns The context snapshot or null if not found
|
|
1320
1448
|
*/
|
|
1321
|
-
export async function readSessionContext(
|
|
1322
|
-
const contextPath = getSessionContextPath(
|
|
1449
|
+
export async function readSessionContext(sessionsDir, sessionId, iteration) {
|
|
1450
|
+
const contextPath = getSessionContextPath(sessionsDir, sessionId, iteration);
|
|
1323
1451
|
try {
|
|
1324
1452
|
const content = await fsPromises.readFile(contextPath, "utf-8");
|
|
1325
1453
|
return JSON.parse(content);
|
|
@@ -1333,12 +1461,12 @@ export async function readSessionContext(specDir, sessionId, iteration) {
|
|
|
1333
1461
|
*
|
|
1334
1462
|
* AC: @session-log-show ac-7, ac-8, ac-9
|
|
1335
1463
|
*
|
|
1336
|
-
* @param
|
|
1464
|
+
* @param sessionsDir - The .kspec directory path
|
|
1337
1465
|
* @param idOrPrefix - Full session ID or prefix (e.g., first 8 chars)
|
|
1338
1466
|
* @returns Resolution result
|
|
1339
1467
|
*/
|
|
1340
|
-
export async function resolveSessionId(
|
|
1341
|
-
const sessionIds = await listSessions(
|
|
1468
|
+
export async function resolveSessionId(sessionsDir, idOrPrefix) {
|
|
1469
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
1342
1470
|
// First, try exact match
|
|
1343
1471
|
if (sessionIds.includes(idOrPrefix)) {
|
|
1344
1472
|
return { ok: true, id: idOrPrefix };
|
|
@@ -1357,8 +1485,8 @@ export async function resolveSessionId(specDir, idOrPrefix) {
|
|
|
1357
1485
|
/**
|
|
1358
1486
|
* Get iteration number from a context snapshot file.
|
|
1359
1487
|
*/
|
|
1360
|
-
async function getIterationNumbers(
|
|
1361
|
-
const sessionDir = getSessionDir(
|
|
1488
|
+
async function getIterationNumbers(sessionsDir, sessionId) {
|
|
1489
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
1362
1490
|
try {
|
|
1363
1491
|
const entries = await fsPromises.readdir(sessionDir);
|
|
1364
1492
|
const iterations = [];
|
|
@@ -1550,14 +1678,14 @@ function boundaryIterationGrouping(events, boundaries) {
|
|
|
1550
1678
|
*
|
|
1551
1679
|
* AC: @session-log-show ac-2, ac-10
|
|
1552
1680
|
*/
|
|
1553
|
-
async function computeIterationSummaries(
|
|
1554
|
-
const events = await readEvents(
|
|
1681
|
+
async function computeIterationSummaries(sessionsDir, sessionId) {
|
|
1682
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
1555
1683
|
const boundaries = findIterationBoundaries(events);
|
|
1556
1684
|
if (boundaries.length > 0) {
|
|
1557
1685
|
return boundaryIterationGrouping(events, boundaries);
|
|
1558
1686
|
}
|
|
1559
1687
|
// Legacy fallback: no prompt.sent boundaries with phase "task-work"
|
|
1560
|
-
const snapshotIterations = await getIterationNumbers(
|
|
1688
|
+
const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
|
|
1561
1689
|
return legacyIterationGrouping(events, snapshotIterations);
|
|
1562
1690
|
}
|
|
1563
1691
|
/**
|
|
@@ -1568,14 +1696,14 @@ async function computeIterationSummaries(specDir, sessionId) {
|
|
|
1568
1696
|
*
|
|
1569
1697
|
* Falls back to counting context-iter-*.json files when no boundaries exist.
|
|
1570
1698
|
*/
|
|
1571
|
-
async function countIterationsBoundaryAware(
|
|
1572
|
-
const events = await readEvents(
|
|
1699
|
+
async function countIterationsBoundaryAware(sessionsDir, sessionId) {
|
|
1700
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
1573
1701
|
const boundaries = findIterationBoundaries(events);
|
|
1574
1702
|
if (boundaries.length > 0) {
|
|
1575
1703
|
return boundaries.length;
|
|
1576
1704
|
}
|
|
1577
1705
|
// Legacy fallback: count from context snapshots and event data
|
|
1578
|
-
const snapshotIterations = await getIterationNumbers(
|
|
1706
|
+
const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
|
|
1579
1707
|
const allIterations = new Set(snapshotIterations);
|
|
1580
1708
|
for (const event of events) {
|
|
1581
1709
|
const data = event.data;
|
|
@@ -1588,17 +1716,17 @@ async function countIterationsBoundaryAware(specDir, sessionId) {
|
|
|
1588
1716
|
/**
|
|
1589
1717
|
* Get full session detail for session log show.
|
|
1590
1718
|
*
|
|
1591
|
-
* @param
|
|
1719
|
+
* @param sessionsDir - The .kspec directory path
|
|
1592
1720
|
* @param sessionId - Session ID (must be resolved first)
|
|
1593
1721
|
* @returns Session detail or null if not found
|
|
1594
1722
|
*/
|
|
1595
|
-
export async function getSessionLogDetail(
|
|
1596
|
-
const metadata = await getSession(
|
|
1723
|
+
export async function getSessionLogDetail(sessionsDir, sessionId) {
|
|
1724
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1597
1725
|
if (!metadata)
|
|
1598
1726
|
return null;
|
|
1599
1727
|
const [eventCount, iterations] = await Promise.all([
|
|
1600
|
-
countEventLines(
|
|
1601
|
-
computeIterationSummaries(
|
|
1728
|
+
countEventLines(sessionsDir, sessionId),
|
|
1729
|
+
computeIterationSummaries(sessionsDir, sessionId),
|
|
1602
1730
|
]);
|
|
1603
1731
|
const startMs = new Date(metadata.started_at).getTime();
|
|
1604
1732
|
const endMs = metadata.ended_at
|
|
@@ -1650,6 +1778,7 @@ export function computeSessionLogStats(summaries) {
|
|
|
1650
1778
|
abandoned: 0,
|
|
1651
1779
|
timed_out: 0,
|
|
1652
1780
|
failed: 0,
|
|
1781
|
+
stalled: 0,
|
|
1653
1782
|
};
|
|
1654
1783
|
for (const s of summaries) {
|
|
1655
1784
|
totalEvents += s.event_count;
|
|
@@ -1661,7 +1790,7 @@ export function computeSessionLogStats(summaries) {
|
|
|
1661
1790
|
const n = summaries.length;
|
|
1662
1791
|
// Build status breakdown
|
|
1663
1792
|
const statusBreakdown = [];
|
|
1664
|
-
for (const status of ["completed", "active", "abandoned", "timed_out", "failed"]) {
|
|
1793
|
+
for (const status of ["completed", "active", "abandoned", "timed_out", "failed", "stalled"]) {
|
|
1665
1794
|
const count = statusCounts[status] || 0;
|
|
1666
1795
|
if (count > 0) {
|
|
1667
1796
|
statusBreakdown.push({
|
|
@@ -1691,11 +1820,11 @@ export function computeSessionLogStats(summaries) {
|
|
|
1691
1820
|
*
|
|
1692
1821
|
* AC: @session-log-stats ac-6
|
|
1693
1822
|
*/
|
|
1694
|
-
export async function computeToolUsageStats(
|
|
1823
|
+
export async function computeToolUsageStats(sessionsDir, sessionIds, limit = 10) {
|
|
1695
1824
|
const toolCounts = {};
|
|
1696
1825
|
let totalToolCalls = 0;
|
|
1697
1826
|
for (const sessionId of sessionIds) {
|
|
1698
|
-
const eventsPath = getSessionEventsPath(
|
|
1827
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
1699
1828
|
try {
|
|
1700
1829
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1701
1830
|
if (!content.trim())
|
|
@@ -1850,34 +1979,45 @@ function extractContentExcerpt(data, pattern, maxLength = 200) {
|
|
|
1850
1979
|
*
|
|
1851
1980
|
* AC: @session-log-search ac-1, ac-2, ac-3, ac-5, ac-7
|
|
1852
1981
|
*
|
|
1853
|
-
* @param
|
|
1982
|
+
* @param sessionsDir - The .kspec directory path
|
|
1854
1983
|
* @param pattern - Case-insensitive substring to search for
|
|
1855
1984
|
* @param options - Search filtering options
|
|
1856
1985
|
* @returns Array of search results grouped by session
|
|
1857
1986
|
*/
|
|
1858
|
-
export async function searchSessionEvents(
|
|
1987
|
+
export async function searchSessionEvents(sessionsDir, pattern, options = {}) {
|
|
1859
1988
|
// Defense-in-depth: normalize limit to a valid positive integer
|
|
1860
1989
|
const rawLimit = options.limit ?? 50;
|
|
1861
1990
|
const limit = Number.isNaN(rawLimit) || rawLimit <= 0 ? 50 : rawLimit;
|
|
1862
1991
|
const lowerPattern = pattern.toLowerCase();
|
|
1863
1992
|
const resolveBlobs = options.resolveBlobs ?? false;
|
|
1864
|
-
//
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
if (options.sinceDate) {
|
|
1869
|
-
filteredSummaries = filteredSummaries.filter((s) => new Date(s.started_at) >= options.sinceDate);
|
|
1993
|
+
// Use pre-filtered session IDs if provided, otherwise load and filter
|
|
1994
|
+
let filteredSummaries;
|
|
1995
|
+
if (options.sessionSummaries) {
|
|
1996
|
+
filteredSummaries = [...options.sessionSummaries];
|
|
1870
1997
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1998
|
+
else if (options.sessionIds) {
|
|
1999
|
+
const idSet = new Set(options.sessionIds);
|
|
2000
|
+
const allSummaries = await getAllSessionLogSummaries(sessionsDir);
|
|
2001
|
+
filteredSummaries = allSummaries.filter((s) => idSet.has(s.id));
|
|
2002
|
+
}
|
|
2003
|
+
else {
|
|
2004
|
+
const allSummaries = await getAllSessionLogSummaries(sessionsDir);
|
|
2005
|
+
// AC: @session-log-search ac-3 - Pre-filter by --since
|
|
2006
|
+
filteredSummaries = allSummaries;
|
|
2007
|
+
if (options.sinceDate) {
|
|
2008
|
+
filteredSummaries = filteredSummaries.filter((s) => new Date(s.started_at) >= options.sinceDate);
|
|
2009
|
+
}
|
|
2010
|
+
// AC: @session-log-search ac-7 - Pre-filter by --agent
|
|
2011
|
+
if (options.agentType) {
|
|
2012
|
+
filteredSummaries = filteredSummaries.filter((s) => s.agent_type === options.agentType);
|
|
2013
|
+
}
|
|
1874
2014
|
}
|
|
1875
2015
|
const results = [];
|
|
1876
2016
|
let totalMatches = 0;
|
|
1877
2017
|
for (const summary of filteredSummaries) {
|
|
1878
2018
|
if (totalMatches >= limit)
|
|
1879
2019
|
break;
|
|
1880
|
-
const eventsPath = getSessionEventsPath(
|
|
2020
|
+
const eventsPath = getSessionEventsPath(sessionsDir, summary.id);
|
|
1881
2021
|
let content;
|
|
1882
2022
|
try {
|
|
1883
2023
|
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
@@ -1916,7 +2056,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1916
2056
|
}
|
|
1917
2057
|
}
|
|
1918
2058
|
const searchableData = resolveBlobs
|
|
1919
|
-
? await resolveSessionBlobPointers(
|
|
2059
|
+
? await resolveSessionBlobPointers(sessionsDir, summary.id, event.data)
|
|
1920
2060
|
: event.data;
|
|
1921
2061
|
// Verify match in stringified data (not just line, in case pattern
|
|
1922
2062
|
// appears in metadata)
|
|
@@ -1926,6 +2066,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1926
2066
|
// AC: @session-log-search ac-4 - Create match with excerpt
|
|
1927
2067
|
matches.push({
|
|
1928
2068
|
session_id: summary.id,
|
|
2069
|
+
event_seq: typeof event.seq === "number" ? event.seq : -1,
|
|
1929
2070
|
timestamp: event.ts,
|
|
1930
2071
|
event_type: event.type,
|
|
1931
2072
|
content_excerpt: extractContentExcerpt(searchableData, pattern, 200),
|
|
@@ -1960,13 +2101,13 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1960
2101
|
* AC: @session-creation-and-env-injection ac-budget-local
|
|
1961
2102
|
* AC: @session-creation-and-env-injection ac-library
|
|
1962
2103
|
*
|
|
1963
|
-
* @param
|
|
2104
|
+
* @param sessionsDir - The .kspec directory path
|
|
1964
2105
|
* @param input - Session creation parameters
|
|
1965
2106
|
* @returns Session metadata and optional budget (no console output)
|
|
1966
2107
|
*/
|
|
1967
|
-
export async function createSessionWithBudget(
|
|
2108
|
+
export async function createSessionWithBudget(sessionsDir, input) {
|
|
1968
2109
|
// Create session
|
|
1969
|
-
const session = await createSession(
|
|
2110
|
+
const session = await createSession(sessionsDir, {
|
|
1970
2111
|
id: input.id,
|
|
1971
2112
|
agent_type: input.agent_type,
|
|
1972
2113
|
task_id: input.task_id,
|
|
@@ -1974,7 +2115,7 @@ export async function createSessionWithBudget(specDir, input) {
|
|
|
1974
2115
|
// Optionally create budget
|
|
1975
2116
|
let budget = null;
|
|
1976
2117
|
if (input.budget !== undefined && input.budget > 0) {
|
|
1977
|
-
budget = await createBudget(
|
|
2118
|
+
budget = await createBudget(sessionsDir, input.id, input.budget);
|
|
1978
2119
|
}
|
|
1979
2120
|
return {
|
|
1980
2121
|
session_id: input.id,
|
|
@@ -2341,13 +2482,13 @@ export async function removeEnvForAdapter(adapterId, previousValue) {
|
|
|
2341
2482
|
*
|
|
2342
2483
|
* AC: @session-creation-and-env-injection ac-invalid-session
|
|
2343
2484
|
*
|
|
2344
|
-
* @param
|
|
2485
|
+
* @param sessionsDir - The .kspec directory path
|
|
2345
2486
|
* @param sessionId - The session ID to validate
|
|
2346
2487
|
* @returns Validation result with error details if invalid
|
|
2347
2488
|
*/
|
|
2348
|
-
export async function validateSessionId(
|
|
2489
|
+
export async function validateSessionId(sessionsDir, sessionId) {
|
|
2349
2490
|
// Check if session directory exists
|
|
2350
|
-
const exists = await sessionExists(
|
|
2491
|
+
const exists = await sessionExists(sessionsDir, sessionId);
|
|
2351
2492
|
if (!exists) {
|
|
2352
2493
|
return {
|
|
2353
2494
|
valid: false,
|
|
@@ -2356,7 +2497,7 @@ export async function validateSessionId(specDir, sessionId) {
|
|
|
2356
2497
|
};
|
|
2357
2498
|
}
|
|
2358
2499
|
// Try to read and validate session metadata
|
|
2359
|
-
const session = await getSession(
|
|
2500
|
+
const session = await getSession(sessionsDir, sessionId);
|
|
2360
2501
|
if (!session) {
|
|
2361
2502
|
return {
|
|
2362
2503
|
valid: false,
|
|
@@ -2383,24 +2524,24 @@ async function writeBudgetAtomic(filePath, budget) {
|
|
|
2383
2524
|
/**
|
|
2384
2525
|
* Create a budget for a session.
|
|
2385
2526
|
*
|
|
2386
|
-
* Writes budget.json to .kspec
|
|
2527
|
+
* Writes budget.json to .kspec-sessions/{id}/ on the local filesystem
|
|
2387
2528
|
* (NOT committed to shadow branch).
|
|
2388
2529
|
*
|
|
2389
2530
|
* AC: @session-creation-and-env-injection ac-budget
|
|
2390
2531
|
* AC: @session-creation-and-env-injection ac-budget-local
|
|
2391
2532
|
*
|
|
2392
|
-
* @param
|
|
2533
|
+
* @param sessionsDir - The .kspec directory path
|
|
2393
2534
|
* @param sessionId - Session ID
|
|
2394
2535
|
* @param maxPerCycle - Maximum tasks allowed per cycle
|
|
2395
2536
|
* @returns The created budget
|
|
2396
2537
|
*/
|
|
2397
|
-
export async function createBudget(
|
|
2538
|
+
export async function createBudget(sessionsDir, sessionId, maxPerCycle) {
|
|
2398
2539
|
const budget = {
|
|
2399
2540
|
max_per_cycle: maxPerCycle,
|
|
2400
2541
|
started_this_cycle: 0,
|
|
2401
2542
|
};
|
|
2402
2543
|
const validated = TaskBudgetSchema.parse(budget);
|
|
2403
|
-
const budgetPath = getSessionBudgetPath(
|
|
2544
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2404
2545
|
await writeBudgetAtomic(budgetPath, validated);
|
|
2405
2546
|
return validated;
|
|
2406
2547
|
}
|
|
@@ -2409,12 +2550,12 @@ export async function createBudget(specDir, sessionId, maxPerCycle) {
|
|
|
2409
2550
|
*
|
|
2410
2551
|
* AC: @task-budget-enforcement ac-no-budget
|
|
2411
2552
|
*
|
|
2412
|
-
* @param
|
|
2553
|
+
* @param sessionsDir - The .kspec directory path
|
|
2413
2554
|
* @param sessionId - Session ID
|
|
2414
2555
|
* @returns Budget or null if no budget configured (opt-in)
|
|
2415
2556
|
*/
|
|
2416
|
-
export async function getBudget(
|
|
2417
|
-
const budgetPath = getSessionBudgetPath(
|
|
2557
|
+
export async function getBudget(sessionsDir, sessionId) {
|
|
2558
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2418
2559
|
let content;
|
|
2419
2560
|
try {
|
|
2420
2561
|
content = await fsPromises.readFile(budgetPath, "utf-8");
|
|
@@ -2440,16 +2581,24 @@ export async function getBudget(specDir, sessionId) {
|
|
|
2440
2581
|
* AC: @task-budget-enforcement ac-no-budget
|
|
2441
2582
|
* AC: @task-budget-enforcement ac-no-session
|
|
2442
2583
|
*
|
|
2443
|
-
* @param
|
|
2584
|
+
* @param sessionsDir - The .kspec directory path
|
|
2444
2585
|
* @param sessionId - Session ID, or undefined if KSPEC_SESSION_ID not set
|
|
2445
2586
|
* @returns Budget check result
|
|
2446
2587
|
*/
|
|
2447
|
-
export async function checkBudget(
|
|
2588
|
+
export async function checkBudget(sessionsDir, sessionId) {
|
|
2448
2589
|
// AC: @task-budget-enforcement ac-no-session — no session means no check
|
|
2449
2590
|
if (!sessionId) {
|
|
2450
2591
|
return { allowed: true };
|
|
2451
2592
|
}
|
|
2452
|
-
|
|
2593
|
+
// Skip budget enforcement when session exists but is not active (stale KSPEC_SESSION_ID).
|
|
2594
|
+
// A completed, abandoned, timed_out, failed, or stalled session should not block task starts.
|
|
2595
|
+
// If session metadata is missing, proceed with normal budget checks — the budget file
|
|
2596
|
+
// itself is the authority on whether enforcement applies.
|
|
2597
|
+
const session = await getSession(sessionsDir, sessionId);
|
|
2598
|
+
if (session && session.status !== "active") {
|
|
2599
|
+
return { allowed: true };
|
|
2600
|
+
}
|
|
2601
|
+
const budget = await getBudget(sessionsDir, sessionId);
|
|
2453
2602
|
// AC: @task-budget-enforcement ac-no-budget — no budget means no check
|
|
2454
2603
|
if (!budget) {
|
|
2455
2604
|
return { allowed: true };
|
|
@@ -2474,12 +2623,12 @@ export async function checkBudget(specDir, sessionId) {
|
|
|
2474
2623
|
* AC: @task-budget-enforcement ac-increment
|
|
2475
2624
|
* AC: @task-budget-enforcement ac-atomic-write
|
|
2476
2625
|
*
|
|
2477
|
-
* @param
|
|
2626
|
+
* @param sessionsDir - The .kspec directory path
|
|
2478
2627
|
* @param sessionId - Session ID
|
|
2479
2628
|
* @returns Updated budget, or null if no budget configured
|
|
2480
2629
|
*/
|
|
2481
|
-
export async function incrementBudget(
|
|
2482
|
-
const budget = await getBudget(
|
|
2630
|
+
export async function incrementBudget(sessionsDir, sessionId) {
|
|
2631
|
+
const budget = await getBudget(sessionsDir, sessionId);
|
|
2483
2632
|
if (!budget) {
|
|
2484
2633
|
return null;
|
|
2485
2634
|
}
|
|
@@ -2488,7 +2637,7 @@ export async function incrementBudget(specDir, sessionId) {
|
|
|
2488
2637
|
started_this_cycle: budget.started_this_cycle + 1,
|
|
2489
2638
|
};
|
|
2490
2639
|
const validated = TaskBudgetSchema.parse(updated);
|
|
2491
|
-
const budgetPath = getSessionBudgetPath(
|
|
2640
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2492
2641
|
await writeBudgetAtomic(budgetPath, validated);
|
|
2493
2642
|
return validated;
|
|
2494
2643
|
}
|
|
@@ -2501,12 +2650,12 @@ export async function incrementBudget(specDir, sessionId) {
|
|
|
2501
2650
|
* AC: @task-budget-enforcement ac-reset
|
|
2502
2651
|
* AC: @task-budget-enforcement ac-atomic-write
|
|
2503
2652
|
*
|
|
2504
|
-
* @param
|
|
2653
|
+
* @param sessionsDir - The .kspec directory path
|
|
2505
2654
|
* @param sessionId - Session ID
|
|
2506
2655
|
* @returns Updated budget, or null if no budget configured
|
|
2507
2656
|
*/
|
|
2508
|
-
export async function resetBudget(
|
|
2509
|
-
const budget = await getBudget(
|
|
2657
|
+
export async function resetBudget(sessionsDir, sessionId) {
|
|
2658
|
+
const budget = await getBudget(sessionsDir, sessionId);
|
|
2510
2659
|
if (!budget) {
|
|
2511
2660
|
return null;
|
|
2512
2661
|
}
|
|
@@ -2515,7 +2664,7 @@ export async function resetBudget(specDir, sessionId) {
|
|
|
2515
2664
|
started_this_cycle: 0,
|
|
2516
2665
|
};
|
|
2517
2666
|
const validated = TaskBudgetSchema.parse(updated);
|
|
2518
|
-
const budgetPath = getSessionBudgetPath(
|
|
2667
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2519
2668
|
await writeBudgetAtomic(budgetPath, validated);
|
|
2520
2669
|
return validated;
|
|
2521
2670
|
}
|