@kynetic-ai/spec 0.10.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 +150 -10
- package/dist/agent-runtime/dispatch.d.ts.map +1 -1
- package/dist/agent-runtime/dispatch.js +1248 -244
- 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 +172 -60
- 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/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +10 -1
- package/dist/cli/commands/meta.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 +107 -23
- package/dist/daemon/routes/aggregation.ts +184 -0
- package/dist/daemon/routes/inbox.ts +5 -0
- package/dist/daemon/routes/items.ts +167 -0
- package/dist/daemon/routes/meta.ts +141 -1
- package/dist/daemon/routes/plans.ts +147 -0
- 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 +581 -0
- package/dist/daemon/routes/tasks.ts +257 -2
- package/dist/daemon/routes/triage.ts +40 -1
- package/dist/daemon/routes/validation.ts +1 -1
- package/dist/daemon/server.ts +165 -50
- 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/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +4 -2
- package/dist/parser/alignment.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 +663 -116
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +28 -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 +115 -71
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +357 -182
- 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/B25nWFyA.js +5 -0
- package/dist/web-ui/_app/immutable/chunks/B2bcA_Q_.js +1 -0
- 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/B8tYZKAE.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BFGAyJjD.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BG0850zf.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BG8eSzAd.js +1 -0
- 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/{D1ArdqNb.js → Bp5pFYXL.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/BsJFsuAT.js +1 -0
- 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/C0w6WDm5.js +1 -0
- 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/CWUQwB9H.js +1 -0
- 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/CaAJD3dl.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{i-XnOIX0.js → ChB5iyEL.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/ChQD-6N8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/{BCkp8Hs8.js → CqbsoCwA.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/DCeJW50p.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DJtZNgcs.js +1 -0
- 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/DW_subyT.js +2 -0
- 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/Dg_zDpDS.js +1 -0
- 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/DqT6OH_u.js +2 -0
- 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/HNjs76Zz.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/HVMjDi4_.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/P0A_fJvS.js +1 -0
- 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/Xvwhx_F1.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Yyz1XMQA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/dh5HeqUr.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/fZMteyca.js +62 -0
- package/dist/web-ui/_app/immutable/chunks/{D28BF5MJ.js → gPrj-hqC.js} +1 -1
- package/dist/web-ui/_app/immutable/chunks/htcWMiYN.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/oTsvd9y4.js +1 -0
- 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/y4GeEH6k.js +1 -0
- 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 -11
- package/package.json +14 -7
- 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.BxCxvrZR.css +0 -1
- package/dist/web-ui/_app/immutable/chunks/B-CZR0q8.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/B1IR5Su5.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/B_Cvvtc4.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/BtFaGGII.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/Bu8JVsCH.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/C87u-CNA.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/CrFkBTYp.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/D6RtLpzL.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/D7FHSgx2.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DBXrsxZQ.js +0 -2
- package/dist/web-ui/_app/immutable/chunks/Da_hHMuA.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/Do6LchSF.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DoNPtcAw.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DtUbXRZz.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DyFPRlLl.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DzAP8lRM.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/DzVXElzN.js +0 -2
- package/dist/web-ui/_app/immutable/chunks/aoPBFken.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/laxtrUO3.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/q1nIWgqB.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/sTLbk5Nm.js +0 -1
- package/dist/web-ui/_app/immutable/chunks/vwKgQu5P.js +0 -5
- package/dist/web-ui/_app/immutable/entry/app.BCwMcqnT.js +0 -2
- package/dist/web-ui/_app/immutable/entry/start.wKCQH-tt.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/0.CjGVMG74.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/1.B6_AIPan.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/2.q4oCS7Ws.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/3.rTKZf9o2.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/4.DVIDRu1d.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/5.8PtPXIOd.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/6.ZZrTemy_.js +0 -1
- package/dist/web-ui/_app/immutable/nodes/7.IP-gxCxi.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,
|
|
@@ -185,11 +225,35 @@ export async function listSessions(specDir) {
|
|
|
185
225
|
return [];
|
|
186
226
|
}
|
|
187
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Count completed sessions grouped by agent_id.
|
|
230
|
+
*
|
|
231
|
+
* Reads only session metadata (not events.jsonl) for performance.
|
|
232
|
+
* A session counts as "completed" if its status is "completed".
|
|
233
|
+
*
|
|
234
|
+
* @param sessionsDir - The .kspec directory path
|
|
235
|
+
* @returns Map of agent_id to completed session count
|
|
236
|
+
*/
|
|
237
|
+
export async function getCompletedSessionCountsByAgent(sessionsDir) {
|
|
238
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
239
|
+
const counts = {};
|
|
240
|
+
// Read metadata in parallel for performance
|
|
241
|
+
const metadataResults = await Promise.all(sessionIds.map((id) => getSession(sessionsDir, id)));
|
|
242
|
+
for (const metadata of metadataResults) {
|
|
243
|
+
if (!metadata)
|
|
244
|
+
continue;
|
|
245
|
+
if (metadata.status !== "completed")
|
|
246
|
+
continue;
|
|
247
|
+
const agentId = metadata.agent_id ?? metadata.agent_type;
|
|
248
|
+
counts[agentId] = (counts[agentId] || 0) + 1;
|
|
249
|
+
}
|
|
250
|
+
return counts;
|
|
251
|
+
}
|
|
188
252
|
/**
|
|
189
253
|
* Check if a session exists.
|
|
190
254
|
*/
|
|
191
|
-
export async function sessionExists(
|
|
192
|
-
const metadataPath = getSessionMetadataPath(
|
|
255
|
+
export async function sessionExists(sessionsDir, sessionId) {
|
|
256
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
193
257
|
try {
|
|
194
258
|
await fsPromises.access(metadataPath);
|
|
195
259
|
return true;
|
|
@@ -207,13 +271,13 @@ export async function sessionExists(specDir, sessionId) {
|
|
|
207
271
|
*
|
|
208
272
|
* AC: @session-end-loop-signal ac-signal
|
|
209
273
|
*
|
|
210
|
-
* @param
|
|
274
|
+
* @param sessionsDir - The .kspec directory path
|
|
211
275
|
* @param sessionId - Session ID
|
|
212
276
|
* @param reason - Optional reason for ending the loop
|
|
213
277
|
* @returns Updated metadata or null if session not found
|
|
214
278
|
*/
|
|
215
|
-
export async function requestEndLoop(
|
|
216
|
-
const metadata = await getSession(
|
|
279
|
+
export async function requestEndLoop(sessionsDir, sessionId, reason) {
|
|
280
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
217
281
|
if (!metadata) {
|
|
218
282
|
return null;
|
|
219
283
|
}
|
|
@@ -222,7 +286,7 @@ export async function requestEndLoop(specDir, sessionId, reason) {
|
|
|
222
286
|
end_requested: true,
|
|
223
287
|
end_reason: reason,
|
|
224
288
|
};
|
|
225
|
-
const metadataPath = getSessionMetadataPath(
|
|
289
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
226
290
|
const content = YAML.stringify(updated, {
|
|
227
291
|
indent: 2,
|
|
228
292
|
lineWidth: 100,
|
|
@@ -240,12 +304,12 @@ export async function requestEndLoop(specDir, sessionId, reason) {
|
|
|
240
304
|
*
|
|
241
305
|
* AC: @session-end-loop-signal ac-detect
|
|
242
306
|
*
|
|
243
|
-
* @param
|
|
307
|
+
* @param sessionsDir - The .kspec directory path
|
|
244
308
|
* @param sessionId - Session ID
|
|
245
309
|
* @returns Object with requested flag and optional reason, or null if session not found
|
|
246
310
|
*/
|
|
247
|
-
export async function isEndLoopRequested(
|
|
248
|
-
const metadata = await getSession(
|
|
311
|
+
export async function isEndLoopRequested(sessionsDir, sessionId) {
|
|
312
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
249
313
|
if (!metadata) {
|
|
250
314
|
return null;
|
|
251
315
|
}
|
|
@@ -263,30 +327,42 @@ export async function isEndLoopRequested(specDir, sessionId) {
|
|
|
263
327
|
* AC: @session-end-loop-signal ac-session-close-signal
|
|
264
328
|
* AC: @session-end-loop-signal ac-session-close-error
|
|
265
329
|
*
|
|
266
|
-
* @param
|
|
330
|
+
* @param sessionsDir - The .kspec directory path
|
|
267
331
|
* @param sessionId - Session ID
|
|
268
332
|
* @param status - New status (completed or abandoned)
|
|
269
333
|
* @param reason - Reason for closing
|
|
270
334
|
* @returns Updated metadata or null if session not found
|
|
271
335
|
*/
|
|
272
|
-
export async function closeSession(
|
|
273
|
-
const metadata = await getSession(
|
|
336
|
+
export async function closeSession(sessionsDir, sessionId, status, reason) {
|
|
337
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
274
338
|
if (!metadata) {
|
|
275
339
|
return null;
|
|
276
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
|
+
]);
|
|
277
348
|
const updated = {
|
|
278
349
|
...metadata,
|
|
279
350
|
status,
|
|
280
351
|
ended_at: new Date().toISOString(),
|
|
281
352
|
close_reason: reason,
|
|
353
|
+
event_count: eventCount,
|
|
354
|
+
iteration_count: iterationCount,
|
|
355
|
+
tasks_completed: tasksCompleted,
|
|
282
356
|
};
|
|
283
|
-
const metadataPath = getSessionMetadataPath(
|
|
357
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
|
|
284
358
|
const content = YAML.stringify(updated, {
|
|
285
359
|
indent: 2,
|
|
286
360
|
lineWidth: 100,
|
|
287
361
|
sortMapEntries: false,
|
|
288
362
|
});
|
|
289
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})`);
|
|
290
366
|
return updated;
|
|
291
367
|
}
|
|
292
368
|
function isRecord(value) {
|
|
@@ -360,7 +436,7 @@ function shouldExternalizeField(pathSegments, value) {
|
|
|
360
436
|
}
|
|
361
437
|
return false;
|
|
362
438
|
}
|
|
363
|
-
async function createBlobPointer(
|
|
439
|
+
async function createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context) {
|
|
364
440
|
const content = stringifyPayload(value);
|
|
365
441
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
366
442
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
@@ -375,7 +451,7 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
|
|
|
375
451
|
await fsPromises.mkdir(context.blobDir, { recursive: true });
|
|
376
452
|
context.ensuredDir = true;
|
|
377
453
|
}
|
|
378
|
-
const absolutePath = path.join(getSessionDir(
|
|
454
|
+
const absolutePath = path.join(getSessionDir(sessionsDir, sessionId), relativePath);
|
|
379
455
|
await fsPromises.writeFile(absolutePath, content, "utf-8");
|
|
380
456
|
}
|
|
381
457
|
context.createdBlobs = (context.createdBlobs ?? 0) + 1;
|
|
@@ -388,15 +464,15 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
|
|
|
388
464
|
preview: toPreview(content),
|
|
389
465
|
};
|
|
390
466
|
}
|
|
391
|
-
async function externalizeOversizedPayloads(
|
|
467
|
+
async function externalizeOversizedPayloads(sessionsDir, sessionId, seq, value, pathSegments, context) {
|
|
392
468
|
if (isSessionBlobPointer(value)) {
|
|
393
469
|
return value;
|
|
394
470
|
}
|
|
395
471
|
if (shouldExternalizeField(pathSegments, value)) {
|
|
396
|
-
return createBlobPointer(
|
|
472
|
+
return createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context);
|
|
397
473
|
}
|
|
398
474
|
if (Array.isArray(value)) {
|
|
399
|
-
return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(
|
|
475
|
+
return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(sessionsDir, sessionId, seq, entry, [
|
|
400
476
|
...pathSegments,
|
|
401
477
|
String(idx),
|
|
402
478
|
], context)));
|
|
@@ -404,14 +480,14 @@ async function externalizeOversizedPayloads(specDir, sessionId, seq, value, path
|
|
|
404
480
|
if (isRecord(value)) {
|
|
405
481
|
const next = {};
|
|
406
482
|
for (const [key, child] of Object.entries(value)) {
|
|
407
|
-
next[key] = await externalizeOversizedPayloads(
|
|
483
|
+
next[key] = await externalizeOversizedPayloads(sessionsDir, sessionId, seq, child, [...pathSegments, key], context);
|
|
408
484
|
}
|
|
409
485
|
return next;
|
|
410
486
|
}
|
|
411
487
|
return value;
|
|
412
488
|
}
|
|
413
|
-
function resolveBlobAbsolutePath(
|
|
414
|
-
const sessionDir = path.resolve(getSessionDir(
|
|
489
|
+
function resolveBlobAbsolutePath(sessionsDir, sessionId, relativePath) {
|
|
490
|
+
const sessionDir = path.resolve(getSessionDir(sessionsDir, sessionId));
|
|
415
491
|
const absolutePath = path.resolve(sessionDir, relativePath);
|
|
416
492
|
if (absolutePath === sessionDir ||
|
|
417
493
|
absolutePath.startsWith(`${sessionDir}${path.sep}`)) {
|
|
@@ -419,8 +495,8 @@ function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
|
|
|
419
495
|
}
|
|
420
496
|
return null;
|
|
421
497
|
}
|
|
422
|
-
async function resolveBlobPointer(
|
|
423
|
-
const absolutePath = resolveBlobAbsolutePath(
|
|
498
|
+
async function resolveBlobPointer(sessionsDir, sessionId, pointer) {
|
|
499
|
+
const absolutePath = resolveBlobAbsolutePath(sessionsDir, sessionId, pointer.path);
|
|
424
500
|
if (!absolutePath) {
|
|
425
501
|
return { ...pointer, content: pointer.preview };
|
|
426
502
|
}
|
|
@@ -438,17 +514,17 @@ async function resolveBlobPointer(specDir, sessionId, pointer) {
|
|
|
438
514
|
* Default flows keep compact pointer objects (preview-only). This helper powers
|
|
439
515
|
* explicit on-demand blob resolution in session log commands.
|
|
440
516
|
*/
|
|
441
|
-
export async function resolveSessionBlobPointers(
|
|
517
|
+
export async function resolveSessionBlobPointers(sessionsDir, sessionId, value) {
|
|
442
518
|
if (isSessionBlobPointer(value)) {
|
|
443
|
-
return resolveBlobPointer(
|
|
519
|
+
return resolveBlobPointer(sessionsDir, sessionId, value);
|
|
444
520
|
}
|
|
445
521
|
if (Array.isArray(value)) {
|
|
446
|
-
return Promise.all(value.map((entry) => resolveSessionBlobPointers(
|
|
522
|
+
return Promise.all(value.map((entry) => resolveSessionBlobPointers(sessionsDir, sessionId, entry)));
|
|
447
523
|
}
|
|
448
524
|
if (isRecord(value)) {
|
|
449
525
|
const next = {};
|
|
450
526
|
for (const [key, child] of Object.entries(value)) {
|
|
451
|
-
next[key] = await resolveSessionBlobPointers(
|
|
527
|
+
next[key] = await resolveSessionBlobPointers(sessionsDir, sessionId, child);
|
|
452
528
|
}
|
|
453
529
|
return next;
|
|
454
530
|
}
|
|
@@ -482,8 +558,8 @@ function extractLastEventSeq(content) {
|
|
|
482
558
|
* Reads a bounded tail slice for O(1) seq lookup; falls back to full scan only
|
|
483
559
|
* if the tail slice cannot be parsed (for example, partial line boundary).
|
|
484
560
|
*/
|
|
485
|
-
async function getNextEventSeq(
|
|
486
|
-
const eventsPath = getSessionEventsPath(
|
|
561
|
+
async function getNextEventSeq(sessionsDir, sessionId) {
|
|
562
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
487
563
|
let fileHandle = null;
|
|
488
564
|
try {
|
|
489
565
|
fileHandle = await fsPromises.open(eventsPath, "r");
|
|
@@ -537,17 +613,17 @@ async function getNextEventSeq(specDir, sessionId) {
|
|
|
537
613
|
* (single-process, sequential event logging). If concurrent access is needed
|
|
538
614
|
* in the future, consider file locking or an in-memory counter per session.
|
|
539
615
|
*
|
|
540
|
-
* @param
|
|
616
|
+
* @param sessionsDir - The .kspec directory path
|
|
541
617
|
* @param input - Event input (ts and seq are auto-assigned if not provided)
|
|
542
618
|
* @returns The appended event with auto-assigned fields
|
|
543
619
|
*/
|
|
544
|
-
export async function appendEvent(
|
|
545
|
-
const sessionDir = getSessionDir(
|
|
546
|
-
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);
|
|
547
623
|
// Ensure session directory exists (lazy creation)
|
|
548
624
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
549
625
|
// Derive next sequence number from the last stored event.
|
|
550
|
-
const seq = input.seq ?? (await getNextEventSeq(
|
|
626
|
+
const seq = input.seq ?? (await getNextEventSeq(sessionsDir, input.session_id));
|
|
551
627
|
// Build full event
|
|
552
628
|
const event = {
|
|
553
629
|
ts: input.ts ?? Date.now(),
|
|
@@ -559,8 +635,8 @@ export async function appendEvent(specDir, input) {
|
|
|
559
635
|
};
|
|
560
636
|
// AC: @session-events ac-8, ac-9 - Externalize oversized payload fields
|
|
561
637
|
// before writing to events.jsonl.
|
|
562
|
-
const externalizedData = await externalizeOversizedPayloads(
|
|
563
|
-
blobDir: getSessionBlobDir(
|
|
638
|
+
const externalizedData = await externalizeOversizedPayloads(sessionsDir, input.session_id, seq, event.data, [], {
|
|
639
|
+
blobDir: getSessionBlobDir(sessionsDir, input.session_id),
|
|
564
640
|
ensuredDir: false,
|
|
565
641
|
});
|
|
566
642
|
const eventWithGuardrails = {
|
|
@@ -574,10 +650,10 @@ export async function appendEvent(specDir, input) {
|
|
|
574
650
|
let line = JSON.stringify(validated);
|
|
575
651
|
if (Buffer.byteLength(line, "utf-8") > EVENT_LINE_MAX_BYTES) {
|
|
576
652
|
const blobContext = {
|
|
577
|
-
blobDir: getSessionBlobDir(
|
|
653
|
+
blobDir: getSessionBlobDir(sessionsDir, input.session_id),
|
|
578
654
|
ensuredDir: false,
|
|
579
655
|
};
|
|
580
|
-
const fullDataPointer = await createBlobPointer(
|
|
656
|
+
const fullDataPointer = await createBlobPointer(sessionsDir, input.session_id, seq, [], validated.data, blobContext);
|
|
581
657
|
validated = SessionEventSchema.parse({
|
|
582
658
|
...validated,
|
|
583
659
|
data: fullDataPointer,
|
|
@@ -599,10 +675,10 @@ export async function appendEvent(specDir, input) {
|
|
|
599
675
|
* Writes are atomic (temp-file then rename). When dryRun is enabled, no files
|
|
600
676
|
* are modified and no blob files are written.
|
|
601
677
|
*/
|
|
602
|
-
export async function compactSessionEvents(
|
|
678
|
+
export async function compactSessionEvents(sessionsDir, sessionId, options = {}) {
|
|
603
679
|
const dryRun = options.dryRun === true;
|
|
604
680
|
const renameFn = options.renameFn ?? fsPromises.rename;
|
|
605
|
-
const eventsPath = getSessionEventsPath(
|
|
681
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
606
682
|
let content;
|
|
607
683
|
try {
|
|
608
684
|
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
@@ -639,7 +715,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
639
715
|
};
|
|
640
716
|
}
|
|
641
717
|
const blobContext = {
|
|
642
|
-
blobDir: getSessionBlobDir(
|
|
718
|
+
blobDir: getSessionBlobDir(sessionsDir, sessionId),
|
|
643
719
|
ensuredDir: false,
|
|
644
720
|
dryRun,
|
|
645
721
|
createdBlobs: 0,
|
|
@@ -656,14 +732,14 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
656
732
|
throw new Error(`Invalid JSON in events log at line ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
657
733
|
}
|
|
658
734
|
const event = SessionEventSchema.parse(parsed);
|
|
659
|
-
const externalizedData = await externalizeOversizedPayloads(
|
|
735
|
+
const externalizedData = await externalizeOversizedPayloads(sessionsDir, sessionId, event.seq, event.data, [], blobContext);
|
|
660
736
|
let validated = SessionEventSchema.parse({
|
|
661
737
|
...event,
|
|
662
738
|
data: externalizedData,
|
|
663
739
|
});
|
|
664
740
|
let compactedLine = JSON.stringify(validated);
|
|
665
741
|
if (Buffer.byteLength(compactedLine, "utf-8") > EVENT_LINE_MAX_BYTES) {
|
|
666
|
-
const fullDataPointer = await createBlobPointer(
|
|
742
|
+
const fullDataPointer = await createBlobPointer(sessionsDir, sessionId, event.seq, [], validated.data, blobContext);
|
|
667
743
|
validated = SessionEventSchema.parse({
|
|
668
744
|
...validated,
|
|
669
745
|
data: fullDataPointer,
|
|
@@ -688,7 +764,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
688
764
|
};
|
|
689
765
|
}
|
|
690
766
|
if (!dryRun) {
|
|
691
|
-
const sessionDir = getSessionDir(
|
|
767
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
692
768
|
const tmpPath = path.join(sessionDir, `.${EVENTS_FILE}.${process.pid}.${Date.now()}.tmp`);
|
|
693
769
|
try {
|
|
694
770
|
await fsPromises.writeFile(tmpPath, compactedContent, "utf-8");
|
|
@@ -699,6 +775,10 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
699
775
|
throw err;
|
|
700
776
|
}
|
|
701
777
|
}
|
|
778
|
+
// AC: @session-branch-worktree ac-commit-boundaries — commit on compact
|
|
779
|
+
if (!dryRun) {
|
|
780
|
+
await commitAtLifecycleBoundary(sessionsDir, `session: compact (${sessionId})`);
|
|
781
|
+
}
|
|
702
782
|
return {
|
|
703
783
|
events_processed: sourceLines.length,
|
|
704
784
|
blobs_created: blobContext.createdBlobs ?? 0,
|
|
@@ -715,12 +795,12 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
|
715
795
|
*
|
|
716
796
|
* AC-4: Returns all events in sequence order.
|
|
717
797
|
*
|
|
718
|
-
* @param
|
|
798
|
+
* @param sessionsDir - The .kspec directory path
|
|
719
799
|
* @param sessionId - Session ID
|
|
720
800
|
* @returns Array of events sorted by sequence number
|
|
721
801
|
*/
|
|
722
|
-
export async function readEvents(
|
|
723
|
-
const eventsPath = getSessionEventsPath(
|
|
802
|
+
export async function readEvents(sessionsDir, sessionId) {
|
|
803
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
724
804
|
try {
|
|
725
805
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
726
806
|
const lines = content
|
|
@@ -745,6 +825,45 @@ export async function readEvents(specDir, sessionId) {
|
|
|
745
825
|
return [];
|
|
746
826
|
}
|
|
747
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
|
+
}
|
|
748
867
|
/**
|
|
749
868
|
* Deduplicate phased tool_call events.
|
|
750
869
|
*
|
|
@@ -794,14 +913,14 @@ export function deduplicatePhasedToolCalls(events) {
|
|
|
794
913
|
/**
|
|
795
914
|
* Read events within a time range.
|
|
796
915
|
*
|
|
797
|
-
* @param
|
|
916
|
+
* @param sessionsDir - The .kspec directory path
|
|
798
917
|
* @param sessionId - Session ID
|
|
799
918
|
* @param since - Start timestamp (inclusive)
|
|
800
919
|
* @param until - End timestamp (inclusive)
|
|
801
920
|
* @returns Array of events within the time range
|
|
802
921
|
*/
|
|
803
|
-
export async function readEventsSince(
|
|
804
|
-
const events = await readEvents(
|
|
922
|
+
export async function readEventsSince(sessionsDir, sessionId, since, until) {
|
|
923
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
805
924
|
return events.filter((e) => {
|
|
806
925
|
if (e.ts < since)
|
|
807
926
|
return false;
|
|
@@ -813,12 +932,12 @@ export async function readEventsSince(specDir, sessionId, since, until) {
|
|
|
813
932
|
/**
|
|
814
933
|
* Get the last event in a session.
|
|
815
934
|
*
|
|
816
|
-
* @param
|
|
935
|
+
* @param sessionsDir - The .kspec directory path
|
|
817
936
|
* @param sessionId - Session ID
|
|
818
937
|
* @returns The last event or null if no events
|
|
819
938
|
*/
|
|
820
|
-
export async function getLastEvent(
|
|
821
|
-
const events = await readEvents(
|
|
939
|
+
export async function getLastEvent(sessionsDir, sessionId) {
|
|
940
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
822
941
|
if (events.length === 0) {
|
|
823
942
|
return null;
|
|
824
943
|
}
|
|
@@ -906,8 +1025,8 @@ export function resolveStaleSessionCriteria(input) {
|
|
|
906
1025
|
* Unlike readEvents(), this is strict: corrupt events are surfaced as failures
|
|
907
1026
|
* so stale auto-close can skip unsafe sessions.
|
|
908
1027
|
*/
|
|
909
|
-
export async function getSessionActivityForStaleCheck(
|
|
910
|
-
const metadata = await getSession(
|
|
1028
|
+
export async function getSessionActivityForStaleCheck(sessionsDir, sessionId) {
|
|
1029
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
911
1030
|
if (!metadata) {
|
|
912
1031
|
return {
|
|
913
1032
|
ok: false,
|
|
@@ -923,7 +1042,7 @@ export async function getSessionActivityForStaleCheck(specDir, sessionId) {
|
|
|
923
1042
|
detail: `Session ${sessionId} has invalid started_at timestamp`,
|
|
924
1043
|
};
|
|
925
1044
|
}
|
|
926
|
-
const eventsPath = getSessionEventsPath(
|
|
1045
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
927
1046
|
let content;
|
|
928
1047
|
try {
|
|
929
1048
|
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
@@ -1019,21 +1138,21 @@ export function evaluateStaleSession(startedAt, activity, criteria, nowMs = Date
|
|
|
1019
1138
|
eligible,
|
|
1020
1139
|
};
|
|
1021
1140
|
}
|
|
1022
|
-
export async function selectStaleActiveSessions(
|
|
1141
|
+
export async function selectStaleActiveSessions(sessionsDir, criteriaInput = {}, nowMs = Date.now()) {
|
|
1023
1142
|
const criteriaResolved = resolveStaleSessionCriteria(criteriaInput);
|
|
1024
1143
|
if (!criteriaResolved.ok) {
|
|
1025
1144
|
throw new Error(`${criteriaResolved.message}. ${criteriaResolved.guidance}`);
|
|
1026
1145
|
}
|
|
1027
1146
|
const criteria = criteriaResolved.criteria;
|
|
1028
|
-
const sessionIds = await listSessions(
|
|
1147
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
1029
1148
|
const evaluations = [];
|
|
1030
1149
|
const candidates = [];
|
|
1031
1150
|
const skipped = [];
|
|
1032
1151
|
for (const sessionId of sessionIds) {
|
|
1033
|
-
const metadata = await getSession(
|
|
1152
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1034
1153
|
if (!metadata || metadata.status !== "active")
|
|
1035
1154
|
continue;
|
|
1036
|
-
const activityResult = await getSessionActivityForStaleCheck(
|
|
1155
|
+
const activityResult = await getSessionActivityForStaleCheck(sessionsDir, sessionId);
|
|
1037
1156
|
if (!activityResult.ok) {
|
|
1038
1157
|
skipped.push({
|
|
1039
1158
|
sessionId,
|
|
@@ -1081,9 +1200,9 @@ export function buildAutoAbandonedCloseReason(criteria, evaluation) {
|
|
|
1081
1200
|
* Apply abandoned metadata to stale session candidates.
|
|
1082
1201
|
*
|
|
1083
1202
|
* All updates in a single invocation share one ended_at timestamp, which lets
|
|
1084
|
-
* the caller persist and commit the batch atomically
|
|
1203
|
+
* the caller persist and commit the batch atomically.
|
|
1085
1204
|
*/
|
|
1086
|
-
export async function applyAutoAbandonMetadata(
|
|
1205
|
+
export async function applyAutoAbandonMetadata(sessionsDir, selection, options) {
|
|
1087
1206
|
const dryRun = options?.dryRun === true;
|
|
1088
1207
|
const endedAt = new Date(options?.nowMs ?? Date.now()).toISOString();
|
|
1089
1208
|
const updates = [];
|
|
@@ -1097,7 +1216,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
|
1097
1216
|
});
|
|
1098
1217
|
if (dryRun)
|
|
1099
1218
|
continue;
|
|
1100
|
-
const metadata = await getSession(
|
|
1219
|
+
const metadata = await getSession(sessionsDir, candidate.sessionId);
|
|
1101
1220
|
if (!metadata)
|
|
1102
1221
|
continue;
|
|
1103
1222
|
const updated = {
|
|
@@ -1106,7 +1225,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
|
1106
1225
|
ended_at: endedAt,
|
|
1107
1226
|
close_reason: closeReason,
|
|
1108
1227
|
};
|
|
1109
|
-
const metadataPath = getSessionMetadataPath(
|
|
1228
|
+
const metadataPath = getSessionMetadataPath(sessionsDir, candidate.sessionId);
|
|
1110
1229
|
const content = YAML.stringify(updated, {
|
|
1111
1230
|
indent: 2,
|
|
1112
1231
|
lineWidth: 100,
|
|
@@ -1114,15 +1233,14 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
|
1114
1233
|
});
|
|
1115
1234
|
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
1116
1235
|
}
|
|
1117
|
-
|
|
1118
|
-
if (!dryRun && updates.length > 0
|
|
1119
|
-
|
|
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)`);
|
|
1120
1239
|
}
|
|
1121
1240
|
return {
|
|
1122
1241
|
dryRun,
|
|
1123
1242
|
updatedCount: updates.length,
|
|
1124
1243
|
updates,
|
|
1125
|
-
shadowCommitted,
|
|
1126
1244
|
};
|
|
1127
1245
|
}
|
|
1128
1246
|
// ─── Session Log Summaries ───────────────────────────────────────────────────
|
|
@@ -1143,8 +1261,8 @@ function resolveSessionType(metadata) {
|
|
|
1143
1261
|
* Count lines in events.jsonl without parsing JSON.
|
|
1144
1262
|
* Much faster than readEvents() for large files.
|
|
1145
1263
|
*/
|
|
1146
|
-
async function countEventLines(
|
|
1147
|
-
const eventsPath = getSessionEventsPath(
|
|
1264
|
+
async function countEventLines(sessionsDir, sessionId) {
|
|
1265
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
1148
1266
|
try {
|
|
1149
1267
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1150
1268
|
if (!content.trim())
|
|
@@ -1158,8 +1276,8 @@ async function countEventLines(specDir, sessionId) {
|
|
|
1158
1276
|
/**
|
|
1159
1277
|
* Count context-iter-*.json files for a session (iteration count).
|
|
1160
1278
|
*/
|
|
1161
|
-
async function countIterations(
|
|
1162
|
-
const sessionDir = getSessionDir(
|
|
1279
|
+
async function countIterations(sessionsDir, sessionId) {
|
|
1280
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
1163
1281
|
try {
|
|
1164
1282
|
const entries = await fsPromises.readdir(sessionDir);
|
|
1165
1283
|
return entries.filter((e) => e.startsWith("context-iter-") && e.endsWith(".json")).length;
|
|
@@ -1181,8 +1299,8 @@ async function countIterations(specDir, sessionId) {
|
|
|
1181
1299
|
*
|
|
1182
1300
|
* We use a fast substring check before JSON parsing for performance.
|
|
1183
1301
|
*/
|
|
1184
|
-
async function countTaskCompletions(
|
|
1185
|
-
const eventsPath = getSessionEventsPath(
|
|
1302
|
+
async function countTaskCompletions(sessionsDir, sessionId) {
|
|
1303
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
1186
1304
|
try {
|
|
1187
1305
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1188
1306
|
if (!content.trim())
|
|
@@ -1221,18 +1339,18 @@ async function countTaskCompletions(specDir, sessionId) {
|
|
|
1221
1339
|
*
|
|
1222
1340
|
* Gathers metadata and computes metrics lazily (only parses what's needed).
|
|
1223
1341
|
*
|
|
1224
|
-
* @param
|
|
1342
|
+
* @param sessionsDir - The .kspec directory path
|
|
1225
1343
|
* @param sessionId - Session ID
|
|
1226
1344
|
* @returns Session summary or null if session doesn't exist
|
|
1227
1345
|
*/
|
|
1228
|
-
export async function getSessionLogSummary(
|
|
1229
|
-
const metadata = await getSession(
|
|
1346
|
+
export async function getSessionLogSummary(sessionsDir, sessionId) {
|
|
1347
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1230
1348
|
if (!metadata)
|
|
1231
1349
|
return null;
|
|
1232
1350
|
const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
|
|
1233
|
-
countEventLines(
|
|
1234
|
-
countIterationsBoundaryAware(
|
|
1235
|
-
countTaskCompletions(
|
|
1351
|
+
countEventLines(sessionsDir, sessionId),
|
|
1352
|
+
countIterationsBoundaryAware(sessionsDir, sessionId),
|
|
1353
|
+
countTaskCompletions(sessionsDir, sessionId),
|
|
1236
1354
|
]);
|
|
1237
1355
|
const startMs = new Date(metadata.started_at).getTime();
|
|
1238
1356
|
const endMs = metadata.ended_at
|
|
@@ -1243,7 +1361,10 @@ export async function getSessionLogSummary(specDir, sessionId) {
|
|
|
1243
1361
|
id: metadata.id,
|
|
1244
1362
|
status: metadata.status,
|
|
1245
1363
|
agent_type: metadata.agent_type,
|
|
1364
|
+
agent_id: metadata.agent_id,
|
|
1246
1365
|
session_type: resolveSessionType(metadata),
|
|
1366
|
+
trigger: metadata.trigger,
|
|
1367
|
+
task_id: metadata.task_id,
|
|
1247
1368
|
started_at: metadata.started_at,
|
|
1248
1369
|
ended_at: metadata.ended_at,
|
|
1249
1370
|
duration_ms: durationMs,
|
|
@@ -1252,15 +1373,48 @@ export async function getSessionLogSummary(specDir, sessionId) {
|
|
|
1252
1373
|
tasks_completed: tasksCompleted,
|
|
1253
1374
|
};
|
|
1254
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
|
+
}
|
|
1255
1409
|
/**
|
|
1256
1410
|
* Get summaries for all sessions.
|
|
1257
1411
|
*
|
|
1258
|
-
* @param
|
|
1412
|
+
* @param sessionsDir - The .kspec directory path
|
|
1259
1413
|
* @returns Array of session summaries
|
|
1260
1414
|
*/
|
|
1261
|
-
export async function getAllSessionLogSummaries(
|
|
1262
|
-
const sessionIds = await listSessions(
|
|
1263
|
-
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)));
|
|
1264
1418
|
return summaries.filter((s) => s !== null);
|
|
1265
1419
|
}
|
|
1266
1420
|
// ─── Context Snapshots ───────────────────────────────────────────────────────
|
|
@@ -1270,14 +1424,14 @@ export async function getAllSessionLogSummaries(specDir) {
|
|
|
1270
1424
|
* This creates an audit trail of what context the agent saw at each iteration,
|
|
1271
1425
|
* useful for debugging and reviewing agent behavior.
|
|
1272
1426
|
*
|
|
1273
|
-
* @param
|
|
1427
|
+
* @param sessionsDir - The .kspec directory path
|
|
1274
1428
|
* @param sessionId - Session ID
|
|
1275
1429
|
* @param iteration - Iteration number
|
|
1276
1430
|
* @param context - The session context data
|
|
1277
1431
|
*/
|
|
1278
|
-
export async function saveSessionContext(
|
|
1279
|
-
const sessionDir = getSessionDir(
|
|
1280
|
-
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);
|
|
1281
1435
|
// Ensure session directory exists
|
|
1282
1436
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
1283
1437
|
// Write context snapshot as pretty JSON
|
|
@@ -1287,13 +1441,13 @@ export async function saveSessionContext(specDir, sessionId, iteration, context)
|
|
|
1287
1441
|
/**
|
|
1288
1442
|
* Read session context snapshot for a given iteration.
|
|
1289
1443
|
*
|
|
1290
|
-
* @param
|
|
1444
|
+
* @param sessionsDir - The .kspec directory path
|
|
1291
1445
|
* @param sessionId - Session ID
|
|
1292
1446
|
* @param iteration - Iteration number
|
|
1293
1447
|
* @returns The context snapshot or null if not found
|
|
1294
1448
|
*/
|
|
1295
|
-
export async function readSessionContext(
|
|
1296
|
-
const contextPath = getSessionContextPath(
|
|
1449
|
+
export async function readSessionContext(sessionsDir, sessionId, iteration) {
|
|
1450
|
+
const contextPath = getSessionContextPath(sessionsDir, sessionId, iteration);
|
|
1297
1451
|
try {
|
|
1298
1452
|
const content = await fsPromises.readFile(contextPath, "utf-8");
|
|
1299
1453
|
return JSON.parse(content);
|
|
@@ -1307,12 +1461,12 @@ export async function readSessionContext(specDir, sessionId, iteration) {
|
|
|
1307
1461
|
*
|
|
1308
1462
|
* AC: @session-log-show ac-7, ac-8, ac-9
|
|
1309
1463
|
*
|
|
1310
|
-
* @param
|
|
1464
|
+
* @param sessionsDir - The .kspec directory path
|
|
1311
1465
|
* @param idOrPrefix - Full session ID or prefix (e.g., first 8 chars)
|
|
1312
1466
|
* @returns Resolution result
|
|
1313
1467
|
*/
|
|
1314
|
-
export async function resolveSessionId(
|
|
1315
|
-
const sessionIds = await listSessions(
|
|
1468
|
+
export async function resolveSessionId(sessionsDir, idOrPrefix) {
|
|
1469
|
+
const sessionIds = await listSessions(sessionsDir);
|
|
1316
1470
|
// First, try exact match
|
|
1317
1471
|
if (sessionIds.includes(idOrPrefix)) {
|
|
1318
1472
|
return { ok: true, id: idOrPrefix };
|
|
@@ -1331,8 +1485,8 @@ export async function resolveSessionId(specDir, idOrPrefix) {
|
|
|
1331
1485
|
/**
|
|
1332
1486
|
* Get iteration number from a context snapshot file.
|
|
1333
1487
|
*/
|
|
1334
|
-
async function getIterationNumbers(
|
|
1335
|
-
const sessionDir = getSessionDir(
|
|
1488
|
+
async function getIterationNumbers(sessionsDir, sessionId) {
|
|
1489
|
+
const sessionDir = getSessionDir(sessionsDir, sessionId);
|
|
1336
1490
|
try {
|
|
1337
1491
|
const entries = await fsPromises.readdir(sessionDir);
|
|
1338
1492
|
const iterations = [];
|
|
@@ -1524,14 +1678,14 @@ function boundaryIterationGrouping(events, boundaries) {
|
|
|
1524
1678
|
*
|
|
1525
1679
|
* AC: @session-log-show ac-2, ac-10
|
|
1526
1680
|
*/
|
|
1527
|
-
async function computeIterationSummaries(
|
|
1528
|
-
const events = await readEvents(
|
|
1681
|
+
async function computeIterationSummaries(sessionsDir, sessionId) {
|
|
1682
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
1529
1683
|
const boundaries = findIterationBoundaries(events);
|
|
1530
1684
|
if (boundaries.length > 0) {
|
|
1531
1685
|
return boundaryIterationGrouping(events, boundaries);
|
|
1532
1686
|
}
|
|
1533
1687
|
// Legacy fallback: no prompt.sent boundaries with phase "task-work"
|
|
1534
|
-
const snapshotIterations = await getIterationNumbers(
|
|
1688
|
+
const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
|
|
1535
1689
|
return legacyIterationGrouping(events, snapshotIterations);
|
|
1536
1690
|
}
|
|
1537
1691
|
/**
|
|
@@ -1542,14 +1696,14 @@ async function computeIterationSummaries(specDir, sessionId) {
|
|
|
1542
1696
|
*
|
|
1543
1697
|
* Falls back to counting context-iter-*.json files when no boundaries exist.
|
|
1544
1698
|
*/
|
|
1545
|
-
async function countIterationsBoundaryAware(
|
|
1546
|
-
const events = await readEvents(
|
|
1699
|
+
async function countIterationsBoundaryAware(sessionsDir, sessionId) {
|
|
1700
|
+
const events = await readEvents(sessionsDir, sessionId);
|
|
1547
1701
|
const boundaries = findIterationBoundaries(events);
|
|
1548
1702
|
if (boundaries.length > 0) {
|
|
1549
1703
|
return boundaries.length;
|
|
1550
1704
|
}
|
|
1551
1705
|
// Legacy fallback: count from context snapshots and event data
|
|
1552
|
-
const snapshotIterations = await getIterationNumbers(
|
|
1706
|
+
const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
|
|
1553
1707
|
const allIterations = new Set(snapshotIterations);
|
|
1554
1708
|
for (const event of events) {
|
|
1555
1709
|
const data = event.data;
|
|
@@ -1562,17 +1716,17 @@ async function countIterationsBoundaryAware(specDir, sessionId) {
|
|
|
1562
1716
|
/**
|
|
1563
1717
|
* Get full session detail for session log show.
|
|
1564
1718
|
*
|
|
1565
|
-
* @param
|
|
1719
|
+
* @param sessionsDir - The .kspec directory path
|
|
1566
1720
|
* @param sessionId - Session ID (must be resolved first)
|
|
1567
1721
|
* @returns Session detail or null if not found
|
|
1568
1722
|
*/
|
|
1569
|
-
export async function getSessionLogDetail(
|
|
1570
|
-
const metadata = await getSession(
|
|
1723
|
+
export async function getSessionLogDetail(sessionsDir, sessionId) {
|
|
1724
|
+
const metadata = await getSession(sessionsDir, sessionId);
|
|
1571
1725
|
if (!metadata)
|
|
1572
1726
|
return null;
|
|
1573
1727
|
const [eventCount, iterations] = await Promise.all([
|
|
1574
|
-
countEventLines(
|
|
1575
|
-
computeIterationSummaries(
|
|
1728
|
+
countEventLines(sessionsDir, sessionId),
|
|
1729
|
+
computeIterationSummaries(sessionsDir, sessionId),
|
|
1576
1730
|
]);
|
|
1577
1731
|
const startMs = new Date(metadata.started_at).getTime();
|
|
1578
1732
|
const endMs = metadata.ended_at
|
|
@@ -1624,6 +1778,7 @@ export function computeSessionLogStats(summaries) {
|
|
|
1624
1778
|
abandoned: 0,
|
|
1625
1779
|
timed_out: 0,
|
|
1626
1780
|
failed: 0,
|
|
1781
|
+
stalled: 0,
|
|
1627
1782
|
};
|
|
1628
1783
|
for (const s of summaries) {
|
|
1629
1784
|
totalEvents += s.event_count;
|
|
@@ -1635,7 +1790,7 @@ export function computeSessionLogStats(summaries) {
|
|
|
1635
1790
|
const n = summaries.length;
|
|
1636
1791
|
// Build status breakdown
|
|
1637
1792
|
const statusBreakdown = [];
|
|
1638
|
-
for (const status of ["completed", "active", "abandoned", "timed_out", "failed"]) {
|
|
1793
|
+
for (const status of ["completed", "active", "abandoned", "timed_out", "failed", "stalled"]) {
|
|
1639
1794
|
const count = statusCounts[status] || 0;
|
|
1640
1795
|
if (count > 0) {
|
|
1641
1796
|
statusBreakdown.push({
|
|
@@ -1665,11 +1820,11 @@ export function computeSessionLogStats(summaries) {
|
|
|
1665
1820
|
*
|
|
1666
1821
|
* AC: @session-log-stats ac-6
|
|
1667
1822
|
*/
|
|
1668
|
-
export async function computeToolUsageStats(
|
|
1823
|
+
export async function computeToolUsageStats(sessionsDir, sessionIds, limit = 10) {
|
|
1669
1824
|
const toolCounts = {};
|
|
1670
1825
|
let totalToolCalls = 0;
|
|
1671
1826
|
for (const sessionId of sessionIds) {
|
|
1672
|
-
const eventsPath = getSessionEventsPath(
|
|
1827
|
+
const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
|
|
1673
1828
|
try {
|
|
1674
1829
|
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1675
1830
|
if (!content.trim())
|
|
@@ -1824,34 +1979,45 @@ function extractContentExcerpt(data, pattern, maxLength = 200) {
|
|
|
1824
1979
|
*
|
|
1825
1980
|
* AC: @session-log-search ac-1, ac-2, ac-3, ac-5, ac-7
|
|
1826
1981
|
*
|
|
1827
|
-
* @param
|
|
1982
|
+
* @param sessionsDir - The .kspec directory path
|
|
1828
1983
|
* @param pattern - Case-insensitive substring to search for
|
|
1829
1984
|
* @param options - Search filtering options
|
|
1830
1985
|
* @returns Array of search results grouped by session
|
|
1831
1986
|
*/
|
|
1832
|
-
export async function searchSessionEvents(
|
|
1987
|
+
export async function searchSessionEvents(sessionsDir, pattern, options = {}) {
|
|
1833
1988
|
// Defense-in-depth: normalize limit to a valid positive integer
|
|
1834
1989
|
const rawLimit = options.limit ?? 50;
|
|
1835
1990
|
const limit = Number.isNaN(rawLimit) || rawLimit <= 0 ? 50 : rawLimit;
|
|
1836
1991
|
const lowerPattern = pattern.toLowerCase();
|
|
1837
1992
|
const resolveBlobs = options.resolveBlobs ?? false;
|
|
1838
|
-
//
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
if (options.sinceDate) {
|
|
1843
|
-
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];
|
|
1844
1997
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
+
}
|
|
1848
2014
|
}
|
|
1849
2015
|
const results = [];
|
|
1850
2016
|
let totalMatches = 0;
|
|
1851
2017
|
for (const summary of filteredSummaries) {
|
|
1852
2018
|
if (totalMatches >= limit)
|
|
1853
2019
|
break;
|
|
1854
|
-
const eventsPath = getSessionEventsPath(
|
|
2020
|
+
const eventsPath = getSessionEventsPath(sessionsDir, summary.id);
|
|
1855
2021
|
let content;
|
|
1856
2022
|
try {
|
|
1857
2023
|
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
@@ -1890,7 +2056,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1890
2056
|
}
|
|
1891
2057
|
}
|
|
1892
2058
|
const searchableData = resolveBlobs
|
|
1893
|
-
? await resolveSessionBlobPointers(
|
|
2059
|
+
? await resolveSessionBlobPointers(sessionsDir, summary.id, event.data)
|
|
1894
2060
|
: event.data;
|
|
1895
2061
|
// Verify match in stringified data (not just line, in case pattern
|
|
1896
2062
|
// appears in metadata)
|
|
@@ -1900,6 +2066,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1900
2066
|
// AC: @session-log-search ac-4 - Create match with excerpt
|
|
1901
2067
|
matches.push({
|
|
1902
2068
|
session_id: summary.id,
|
|
2069
|
+
event_seq: typeof event.seq === "number" ? event.seq : -1,
|
|
1903
2070
|
timestamp: event.ts,
|
|
1904
2071
|
event_type: event.type,
|
|
1905
2072
|
content_excerpt: extractContentExcerpt(searchableData, pattern, 200),
|
|
@@ -1934,13 +2101,13 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1934
2101
|
* AC: @session-creation-and-env-injection ac-budget-local
|
|
1935
2102
|
* AC: @session-creation-and-env-injection ac-library
|
|
1936
2103
|
*
|
|
1937
|
-
* @param
|
|
2104
|
+
* @param sessionsDir - The .kspec directory path
|
|
1938
2105
|
* @param input - Session creation parameters
|
|
1939
2106
|
* @returns Session metadata and optional budget (no console output)
|
|
1940
2107
|
*/
|
|
1941
|
-
export async function createSessionWithBudget(
|
|
2108
|
+
export async function createSessionWithBudget(sessionsDir, input) {
|
|
1942
2109
|
// Create session
|
|
1943
|
-
const session = await createSession(
|
|
2110
|
+
const session = await createSession(sessionsDir, {
|
|
1944
2111
|
id: input.id,
|
|
1945
2112
|
agent_type: input.agent_type,
|
|
1946
2113
|
task_id: input.task_id,
|
|
@@ -1948,7 +2115,7 @@ export async function createSessionWithBudget(specDir, input) {
|
|
|
1948
2115
|
// Optionally create budget
|
|
1949
2116
|
let budget = null;
|
|
1950
2117
|
if (input.budget !== undefined && input.budget > 0) {
|
|
1951
|
-
budget = await createBudget(
|
|
2118
|
+
budget = await createBudget(sessionsDir, input.id, input.budget);
|
|
1952
2119
|
}
|
|
1953
2120
|
return {
|
|
1954
2121
|
session_id: input.id,
|
|
@@ -2315,13 +2482,13 @@ export async function removeEnvForAdapter(adapterId, previousValue) {
|
|
|
2315
2482
|
*
|
|
2316
2483
|
* AC: @session-creation-and-env-injection ac-invalid-session
|
|
2317
2484
|
*
|
|
2318
|
-
* @param
|
|
2485
|
+
* @param sessionsDir - The .kspec directory path
|
|
2319
2486
|
* @param sessionId - The session ID to validate
|
|
2320
2487
|
* @returns Validation result with error details if invalid
|
|
2321
2488
|
*/
|
|
2322
|
-
export async function validateSessionId(
|
|
2489
|
+
export async function validateSessionId(sessionsDir, sessionId) {
|
|
2323
2490
|
// Check if session directory exists
|
|
2324
|
-
const exists = await sessionExists(
|
|
2491
|
+
const exists = await sessionExists(sessionsDir, sessionId);
|
|
2325
2492
|
if (!exists) {
|
|
2326
2493
|
return {
|
|
2327
2494
|
valid: false,
|
|
@@ -2330,7 +2497,7 @@ export async function validateSessionId(specDir, sessionId) {
|
|
|
2330
2497
|
};
|
|
2331
2498
|
}
|
|
2332
2499
|
// Try to read and validate session metadata
|
|
2333
|
-
const session = await getSession(
|
|
2500
|
+
const session = await getSession(sessionsDir, sessionId);
|
|
2334
2501
|
if (!session) {
|
|
2335
2502
|
return {
|
|
2336
2503
|
valid: false,
|
|
@@ -2357,24 +2524,24 @@ async function writeBudgetAtomic(filePath, budget) {
|
|
|
2357
2524
|
/**
|
|
2358
2525
|
* Create a budget for a session.
|
|
2359
2526
|
*
|
|
2360
|
-
* Writes budget.json to .kspec
|
|
2527
|
+
* Writes budget.json to .kspec-sessions/{id}/ on the local filesystem
|
|
2361
2528
|
* (NOT committed to shadow branch).
|
|
2362
2529
|
*
|
|
2363
2530
|
* AC: @session-creation-and-env-injection ac-budget
|
|
2364
2531
|
* AC: @session-creation-and-env-injection ac-budget-local
|
|
2365
2532
|
*
|
|
2366
|
-
* @param
|
|
2533
|
+
* @param sessionsDir - The .kspec directory path
|
|
2367
2534
|
* @param sessionId - Session ID
|
|
2368
2535
|
* @param maxPerCycle - Maximum tasks allowed per cycle
|
|
2369
2536
|
* @returns The created budget
|
|
2370
2537
|
*/
|
|
2371
|
-
export async function createBudget(
|
|
2538
|
+
export async function createBudget(sessionsDir, sessionId, maxPerCycle) {
|
|
2372
2539
|
const budget = {
|
|
2373
2540
|
max_per_cycle: maxPerCycle,
|
|
2374
2541
|
started_this_cycle: 0,
|
|
2375
2542
|
};
|
|
2376
2543
|
const validated = TaskBudgetSchema.parse(budget);
|
|
2377
|
-
const budgetPath = getSessionBudgetPath(
|
|
2544
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2378
2545
|
await writeBudgetAtomic(budgetPath, validated);
|
|
2379
2546
|
return validated;
|
|
2380
2547
|
}
|
|
@@ -2383,12 +2550,12 @@ export async function createBudget(specDir, sessionId, maxPerCycle) {
|
|
|
2383
2550
|
*
|
|
2384
2551
|
* AC: @task-budget-enforcement ac-no-budget
|
|
2385
2552
|
*
|
|
2386
|
-
* @param
|
|
2553
|
+
* @param sessionsDir - The .kspec directory path
|
|
2387
2554
|
* @param sessionId - Session ID
|
|
2388
2555
|
* @returns Budget or null if no budget configured (opt-in)
|
|
2389
2556
|
*/
|
|
2390
|
-
export async function getBudget(
|
|
2391
|
-
const budgetPath = getSessionBudgetPath(
|
|
2557
|
+
export async function getBudget(sessionsDir, sessionId) {
|
|
2558
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2392
2559
|
let content;
|
|
2393
2560
|
try {
|
|
2394
2561
|
content = await fsPromises.readFile(budgetPath, "utf-8");
|
|
@@ -2414,16 +2581,24 @@ export async function getBudget(specDir, sessionId) {
|
|
|
2414
2581
|
* AC: @task-budget-enforcement ac-no-budget
|
|
2415
2582
|
* AC: @task-budget-enforcement ac-no-session
|
|
2416
2583
|
*
|
|
2417
|
-
* @param
|
|
2584
|
+
* @param sessionsDir - The .kspec directory path
|
|
2418
2585
|
* @param sessionId - Session ID, or undefined if KSPEC_SESSION_ID not set
|
|
2419
2586
|
* @returns Budget check result
|
|
2420
2587
|
*/
|
|
2421
|
-
export async function checkBudget(
|
|
2588
|
+
export async function checkBudget(sessionsDir, sessionId) {
|
|
2422
2589
|
// AC: @task-budget-enforcement ac-no-session — no session means no check
|
|
2423
2590
|
if (!sessionId) {
|
|
2424
2591
|
return { allowed: true };
|
|
2425
2592
|
}
|
|
2426
|
-
|
|
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);
|
|
2427
2602
|
// AC: @task-budget-enforcement ac-no-budget — no budget means no check
|
|
2428
2603
|
if (!budget) {
|
|
2429
2604
|
return { allowed: true };
|
|
@@ -2448,12 +2623,12 @@ export async function checkBudget(specDir, sessionId) {
|
|
|
2448
2623
|
* AC: @task-budget-enforcement ac-increment
|
|
2449
2624
|
* AC: @task-budget-enforcement ac-atomic-write
|
|
2450
2625
|
*
|
|
2451
|
-
* @param
|
|
2626
|
+
* @param sessionsDir - The .kspec directory path
|
|
2452
2627
|
* @param sessionId - Session ID
|
|
2453
2628
|
* @returns Updated budget, or null if no budget configured
|
|
2454
2629
|
*/
|
|
2455
|
-
export async function incrementBudget(
|
|
2456
|
-
const budget = await getBudget(
|
|
2630
|
+
export async function incrementBudget(sessionsDir, sessionId) {
|
|
2631
|
+
const budget = await getBudget(sessionsDir, sessionId);
|
|
2457
2632
|
if (!budget) {
|
|
2458
2633
|
return null;
|
|
2459
2634
|
}
|
|
@@ -2462,7 +2637,7 @@ export async function incrementBudget(specDir, sessionId) {
|
|
|
2462
2637
|
started_this_cycle: budget.started_this_cycle + 1,
|
|
2463
2638
|
};
|
|
2464
2639
|
const validated = TaskBudgetSchema.parse(updated);
|
|
2465
|
-
const budgetPath = getSessionBudgetPath(
|
|
2640
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2466
2641
|
await writeBudgetAtomic(budgetPath, validated);
|
|
2467
2642
|
return validated;
|
|
2468
2643
|
}
|
|
@@ -2475,12 +2650,12 @@ export async function incrementBudget(specDir, sessionId) {
|
|
|
2475
2650
|
* AC: @task-budget-enforcement ac-reset
|
|
2476
2651
|
* AC: @task-budget-enforcement ac-atomic-write
|
|
2477
2652
|
*
|
|
2478
|
-
* @param
|
|
2653
|
+
* @param sessionsDir - The .kspec directory path
|
|
2479
2654
|
* @param sessionId - Session ID
|
|
2480
2655
|
* @returns Updated budget, or null if no budget configured
|
|
2481
2656
|
*/
|
|
2482
|
-
export async function resetBudget(
|
|
2483
|
-
const budget = await getBudget(
|
|
2657
|
+
export async function resetBudget(sessionsDir, sessionId) {
|
|
2658
|
+
const budget = await getBudget(sessionsDir, sessionId);
|
|
2484
2659
|
if (!budget) {
|
|
2485
2660
|
return null;
|
|
2486
2661
|
}
|
|
@@ -2489,7 +2664,7 @@ export async function resetBudget(specDir, sessionId) {
|
|
|
2489
2664
|
started_this_cycle: 0,
|
|
2490
2665
|
};
|
|
2491
2666
|
const validated = TaskBudgetSchema.parse(updated);
|
|
2492
|
-
const budgetPath = getSessionBudgetPath(
|
|
2667
|
+
const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
|
|
2493
2668
|
await writeBudgetAtomic(budgetPath, validated);
|
|
2494
2669
|
return validated;
|
|
2495
2670
|
}
|