@kynetic-ai/spec 0.9.1 → 0.10.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 +2 -1
- package/dist/acp/client.d.ts +6 -1
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +7 -2
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +12 -1
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +27 -4
- package/dist/acp/framing.js.map +1 -1
- package/dist/agent-runtime/dispatch.d.ts +261 -0
- package/dist/agent-runtime/dispatch.d.ts.map +1 -0
- package/dist/agent-runtime/dispatch.js +791 -0
- package/dist/agent-runtime/dispatch.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +11 -0
- package/dist/agent-runtime/index.d.ts.map +1 -0
- package/dist/agent-runtime/index.js +11 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/invocation.d.ts +86 -0
- package/dist/agent-runtime/invocation.d.ts.map +1 -0
- package/dist/agent-runtime/invocation.js +442 -0
- package/dist/agent-runtime/invocation.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +50 -0
- package/dist/agent-runtime/prompts.d.ts.map +1 -0
- package/dist/agent-runtime/prompts.js +108 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +60 -4
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-exec.d.ts.map +1 -1
- package/dist/cli/batch-exec.js +140 -62
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/batch-write-buffer.d.ts +141 -0
- package/dist/cli/batch-write-buffer.d.ts.map +1 -0
- package/dist/cli/batch-write-buffer.js +400 -0
- package/dist/cli/batch-write-buffer.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +20 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +831 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +46 -22
- package/dist/cli/commands/inbox.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/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +22 -16
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.js +1 -1
- 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 +159 -6
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +2 -1
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.js +19 -3
- 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 +87 -43
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts +5 -56
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +52 -1502
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +22 -13
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +70 -11
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
- package/dist/cli/commands/session/checkpoint.js +7 -2
- 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 +15 -0
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/context.d.ts.map +1 -1
- package/dist/cli/commands/session/context.js +10 -5
- package/dist/cli/commands/session/context.js.map +1 -1
- package/dist/cli/commands/session/log.d.ts +1 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +124 -8
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/session/stale-close.d.ts +17 -0
- package/dist/cli/commands/session/stale-close.d.ts.map +1 -0
- package/dist/cli/commands/session/stale-close.js +378 -0
- package/dist/cli/commands/session/stale-close.js.map +1 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +95 -0
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts.map +1 -1
- package/dist/cli/commands/skill-crud.js +4 -3
- 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 +15 -0
- 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 +50 -18
- 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 +536 -310
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.js +1 -1
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +37 -13
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +65 -25
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +5 -0
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +5 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/daemon/project-context.ts +22 -0
- package/dist/daemon/routes/agent-dispatch.ts +272 -0
- package/dist/daemon/server.ts +55 -20
- package/dist/daemon/websocket/handler.ts +67 -6
- package/dist/daemon/websocket/lifecycle.ts +19 -0
- package/dist/daemon/websocket/pubsub.ts +74 -3
- package/dist/export/html.d.ts.map +1 -1
- package/dist/export/html.js +5 -2
- package/dist/export/html.js.map +1 -1
- package/dist/export/triage.d.ts +1 -1
- package/dist/export/triage.d.ts.map +1 -1
- package/dist/export/triage.js +5 -3
- package/dist/export/triage.js.map +1 -1
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +6 -3
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.js +1 -1
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +6 -6
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +9 -8
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +12 -12
- package/dist/parser/plans.d.ts +7 -0
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +100 -15
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/refs.d.ts +5 -0
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +17 -12
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/shadow.d.ts +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +71 -4
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +6 -3
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +35 -76
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +24 -5
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +224 -64
- package/dist/parser/yaml.js.map +1 -1
- package/dist/schema/meta.d.ts +442 -119
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +55 -0
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +22 -22
- package/dist/schema/spec.d.ts +39 -39
- package/dist/schema/task.d.ts +43 -32
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +5 -0
- package/dist/schema/task.js.map +1 -1
- package/dist/sessions/store.d.ts +112 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +414 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +75 -17
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +51 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/triage/actions.d.ts +1 -0
- package/dist/triage/actions.d.ts.map +1 -1
- package/dist/triage/actions.js +34 -7
- package/dist/triage/actions.js.map +1 -1
- package/dist/utils/commit.js +1 -1
- package/dist/utils/commit.js.map +1 -1
- package/dist/web-ui/_app/env.js +1 -0
- package/dist/web-ui/_app/immutable/assets/0.BxCxvrZR.css +1 -0
- package/dist/web-ui/_app/immutable/assets/select-trigger.CV-KWLNP.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/B-CZR0q8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B1IR5Su5.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BCkp8Hs8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B_Cvvtc4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BtFaGGII.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Bu8JVsCH.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C87u-CNA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CrFkBTYp.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D1ArdqNb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D28BF5MJ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D6RtLpzL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D7FHSgx2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DBXrsxZQ.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/Da_hHMuA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Do6LchSF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DoNPtcAw.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DtUbXRZz.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DyFPRlLl.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzAP8lRM.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzVXElzN.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/aoPBFken.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/i-XnOIX0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/laxtrUO3.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/q1nIWgqB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/sTLbk5Nm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/vwKgQu5P.js +5 -0
- package/dist/web-ui/_app/immutable/entry/app.BCwMcqnT.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.wKCQH-tt.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.CjGVMG74.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.B6_AIPan.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.q4oCS7Ws.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.rTKZf9o2.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.DVIDRu1d.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/5.8PtPXIOd.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.ZZrTemy_.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.IP-gxCxi.js +1 -0
- package/dist/web-ui/_app/version.json +1 -0
- package/dist/web-ui/index.html +36 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +25 -2
- package/templates/agents-sections/06-ralph-loop.md +64 -11
- package/templates/skills/task-work/SKILL.md +25 -2
- package/dist/ralph/cli-renderer.d.ts +0 -27
- package/dist/ralph/cli-renderer.d.ts.map +0 -1
- package/dist/ralph/cli-renderer.js +0 -250
- package/dist/ralph/cli-renderer.js.map +0 -1
- package/dist/ralph/events.d.ts +0 -65
- package/dist/ralph/events.d.ts.map +0 -1
- package/dist/ralph/events.js +0 -600
- package/dist/ralph/events.js.map +0 -1
- package/dist/ralph/index.d.ts +0 -11
- package/dist/ralph/index.d.ts.map +0 -1
- package/dist/ralph/index.js +0 -16
- package/dist/ralph/index.js.map +0 -1
- package/dist/ralph/loop-errors.d.ts +0 -83
- package/dist/ralph/loop-errors.d.ts.map +0 -1
- package/dist/ralph/loop-errors.js +0 -150
- package/dist/ralph/loop-errors.js.map +0 -1
- package/dist/ralph/subagent.d.ts +0 -127
- package/dist/ralph/subagent.d.ts.map +0 -1
- package/dist/ralph/subagent.js +0 -268
- package/dist/ralph/subagent.js.map +0 -1
- package/dist/ralph/wrap-up.d.ts +0 -127
- package/dist/ralph/wrap-up.d.ts.map +0 -1
- package/dist/ralph/wrap-up.js +0 -271
- package/dist/ralph/wrap-up.js.map +0 -1
package/dist/sessions/store.js
CHANGED
|
@@ -17,6 +17,7 @@ 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";
|
|
20
21
|
import { SessionEventSchema, SessionMetadataSchema, TaskBudgetSchema, } from "./types.js";
|
|
21
22
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
22
23
|
const SESSIONS_DIR = "sessions";
|
|
@@ -29,6 +30,7 @@ const BLOBS_DIR = "blobs";
|
|
|
29
30
|
const EVENT_LINE_MAX_BYTES = 256 * 1024;
|
|
30
31
|
const EVENT_FIELD_EXTERNALIZE_BYTES = 16 * 1024;
|
|
31
32
|
const EVENT_PREVIEW_MAX_BYTES = 512;
|
|
33
|
+
const EVENT_SEQ_TAIL_READ_BYTES = EVENT_LINE_MAX_BYTES + 1024;
|
|
32
34
|
// ─── Path Helpers ────────────────────────────────────────────────────────────
|
|
33
35
|
/**
|
|
34
36
|
* Get the sessions directory path within a spec directory.
|
|
@@ -90,10 +92,13 @@ export async function createSession(specDir, input) {
|
|
|
90
92
|
// Create session directory
|
|
91
93
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
92
94
|
// Build full metadata
|
|
95
|
+
// AC: @session-model-evolution ac-1 — include trigger and agent_id when provided
|
|
93
96
|
const metadata = {
|
|
94
97
|
id: input.id,
|
|
95
98
|
task_id: input.task_id,
|
|
96
99
|
agent_type: input.agent_type,
|
|
100
|
+
agent_id: input.agent_id,
|
|
101
|
+
trigger: input.trigger,
|
|
97
102
|
status: input.status ?? "active",
|
|
98
103
|
started_at: input.started_at ?? new Date().toISOString(),
|
|
99
104
|
ended_at: undefined,
|
|
@@ -120,7 +125,13 @@ export async function getSession(specDir, sessionId) {
|
|
|
120
125
|
try {
|
|
121
126
|
const content = await fsPromises.readFile(metadataPath, "utf-8");
|
|
122
127
|
const raw = YAML.parse(content);
|
|
123
|
-
|
|
128
|
+
const parsed = SessionMetadataSchema.parse(raw);
|
|
129
|
+
// AC: @session-model-evolution ac-2 — materialize defaults for legacy sessions
|
|
130
|
+
return {
|
|
131
|
+
...parsed,
|
|
132
|
+
trigger: parsed.trigger ?? "legacy",
|
|
133
|
+
agent_id: parsed.agent_id ?? parsed.agent_type,
|
|
134
|
+
};
|
|
124
135
|
}
|
|
125
136
|
catch {
|
|
126
137
|
return null;
|
|
@@ -443,22 +454,70 @@ export async function resolveSessionBlobPointers(specDir, sessionId, value) {
|
|
|
443
454
|
}
|
|
444
455
|
return value;
|
|
445
456
|
}
|
|
457
|
+
function extractLastEventSeq(content) {
|
|
458
|
+
const lines = content.split("\n");
|
|
459
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
460
|
+
const line = lines[i].trim();
|
|
461
|
+
if (line.length === 0) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const parsed = JSON.parse(line);
|
|
466
|
+
if (isRecord(parsed) &&
|
|
467
|
+
typeof parsed.seq === "number" &&
|
|
468
|
+
Number.isInteger(parsed.seq) &&
|
|
469
|
+
parsed.seq >= 0) {
|
|
470
|
+
return parsed.seq;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// Ignore malformed lines and continue scanning backward.
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
446
479
|
/**
|
|
447
|
-
* Get
|
|
480
|
+
* Get next sequence number from the last stored event.
|
|
448
481
|
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* @returns Number of events in the session
|
|
482
|
+
* Reads a bounded tail slice for O(1) seq lookup; falls back to full scan only
|
|
483
|
+
* if the tail slice cannot be parsed (for example, partial line boundary).
|
|
452
484
|
*/
|
|
453
|
-
async function
|
|
485
|
+
async function getNextEventSeq(specDir, sessionId) {
|
|
454
486
|
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
487
|
+
let fileHandle = null;
|
|
455
488
|
try {
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
489
|
+
fileHandle = await fsPromises.open(eventsPath, "r");
|
|
490
|
+
const stats = await fileHandle.stat();
|
|
491
|
+
if (stats.size === 0) {
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
const readBytes = Math.min(stats.size, EVENT_SEQ_TAIL_READ_BYTES);
|
|
495
|
+
const startOffset = stats.size - readBytes;
|
|
496
|
+
const buffer = Buffer.alloc(readBytes);
|
|
497
|
+
await fileHandle.read(buffer, 0, readBytes, startOffset);
|
|
498
|
+
let tailContent = buffer.toString("utf-8");
|
|
499
|
+
if (startOffset > 0) {
|
|
500
|
+
// Drop potential partial first line from tail slice.
|
|
501
|
+
const firstNewline = tailContent.indexOf("\n");
|
|
502
|
+
tailContent = firstNewline === -1 ? "" : tailContent.slice(firstNewline + 1);
|
|
503
|
+
}
|
|
504
|
+
const tailSeq = extractLastEventSeq(tailContent);
|
|
505
|
+
if (tailSeq !== null) {
|
|
506
|
+
return tailSeq + 1;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
if (error.code === "ENOENT") {
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
await fileHandle?.close().catch(() => undefined);
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const fullContent = await fsPromises.readFile(eventsPath, "utf-8");
|
|
519
|
+
const fullSeq = extractLastEventSeq(fullContent);
|
|
520
|
+
return fullSeq === null ? 0 : fullSeq + 1;
|
|
462
521
|
}
|
|
463
522
|
catch {
|
|
464
523
|
return 0;
|
|
@@ -487,8 +546,8 @@ export async function appendEvent(specDir, input) {
|
|
|
487
546
|
const eventsPath = getSessionEventsPath(specDir, input.session_id);
|
|
488
547
|
// Ensure session directory exists (lazy creation)
|
|
489
548
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
490
|
-
//
|
|
491
|
-
const seq = input.seq ?? (await
|
|
549
|
+
// Derive next sequence number from the last stored event.
|
|
550
|
+
const seq = input.seq ?? (await getNextEventSeq(specDir, input.session_id));
|
|
492
551
|
// Build full event
|
|
493
552
|
const event = {
|
|
494
553
|
ts: input.ts ?? Date.now(),
|
|
@@ -765,6 +824,321 @@ export async function getLastEvent(specDir, sessionId) {
|
|
|
765
824
|
}
|
|
766
825
|
return events[events.length - 1];
|
|
767
826
|
}
|
|
827
|
+
// ─── Stale Session Candidate Selection ──────────────────────────────────────
|
|
828
|
+
const RELATIVE_DURATION_PATTERN = /^(\d+)([hdwm])$/i;
|
|
829
|
+
const STALE_DEFAULTS = {
|
|
830
|
+
olderThan: "24h",
|
|
831
|
+
inactiveFor: "6h",
|
|
832
|
+
livenessGuard: "5m",
|
|
833
|
+
};
|
|
834
|
+
function durationGuidance(flag) {
|
|
835
|
+
return `--${flag} accepts relative durations only (h, d, w, m), for example 6h, 7d, 2w, 1m`;
|
|
836
|
+
}
|
|
837
|
+
function parseRelativeDurationMs(rawValue) {
|
|
838
|
+
const match = rawValue.match(RELATIVE_DURATION_PATTERN);
|
|
839
|
+
if (!match)
|
|
840
|
+
return null;
|
|
841
|
+
const amount = parseInt(match[1], 10);
|
|
842
|
+
const unit = match[2].toLowerCase();
|
|
843
|
+
if (Number.isNaN(amount))
|
|
844
|
+
return null;
|
|
845
|
+
switch (unit) {
|
|
846
|
+
case "h":
|
|
847
|
+
return amount * 60 * 60 * 1000;
|
|
848
|
+
case "d":
|
|
849
|
+
return amount * 24 * 60 * 60 * 1000;
|
|
850
|
+
case "w":
|
|
851
|
+
return amount * 7 * 24 * 60 * 60 * 1000;
|
|
852
|
+
case "m":
|
|
853
|
+
return amount * 60 * 1000;
|
|
854
|
+
default:
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function parseCriteriaDuration(field, value) {
|
|
859
|
+
const parsed = parseRelativeDurationMs(value);
|
|
860
|
+
if (parsed !== null) {
|
|
861
|
+
return { ok: true, ms: parsed };
|
|
862
|
+
}
|
|
863
|
+
const parsedDate = new Date(value);
|
|
864
|
+
const appearsAbsolute = !Number.isNaN(parsedDate.getTime());
|
|
865
|
+
return {
|
|
866
|
+
ok: false,
|
|
867
|
+
field,
|
|
868
|
+
value,
|
|
869
|
+
message: appearsAbsolute
|
|
870
|
+
? `Invalid value for --${field}: "${value}" (absolute timestamps are not supported)`
|
|
871
|
+
: `Invalid value for --${field}: "${value}"`,
|
|
872
|
+
guidance: durationGuidance(field),
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
export function resolveStaleSessionCriteria(input) {
|
|
876
|
+
const olderThan = input.olderThan ?? STALE_DEFAULTS.olderThan;
|
|
877
|
+
const inactiveFor = input.inactiveFor ?? STALE_DEFAULTS.inactiveFor;
|
|
878
|
+
const livenessGuard = input.livenessGuard ?? STALE_DEFAULTS.livenessGuard;
|
|
879
|
+
const olderThanParsed = parseCriteriaDuration("older-than", olderThan);
|
|
880
|
+
if (!olderThanParsed.ok)
|
|
881
|
+
return olderThanParsed;
|
|
882
|
+
const olderThanMs = olderThanParsed.ms;
|
|
883
|
+
const inactiveForParsed = parseCriteriaDuration("inactive-for", inactiveFor);
|
|
884
|
+
if (!inactiveForParsed.ok)
|
|
885
|
+
return inactiveForParsed;
|
|
886
|
+
const inactiveForMs = inactiveForParsed.ms;
|
|
887
|
+
const livenessGuardParsed = parseCriteriaDuration("liveness-guard", livenessGuard);
|
|
888
|
+
if (!livenessGuardParsed.ok)
|
|
889
|
+
return livenessGuardParsed;
|
|
890
|
+
const livenessGuardMs = livenessGuardParsed.ms;
|
|
891
|
+
return {
|
|
892
|
+
ok: true,
|
|
893
|
+
criteria: {
|
|
894
|
+
olderThan,
|
|
895
|
+
olderThanMs,
|
|
896
|
+
inactiveFor,
|
|
897
|
+
inactiveForMs,
|
|
898
|
+
livenessGuard,
|
|
899
|
+
livenessGuardMs,
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Resolve most recent activity timestamp for stale-session candidate checks.
|
|
905
|
+
*
|
|
906
|
+
* Unlike readEvents(), this is strict: corrupt events are surfaced as failures
|
|
907
|
+
* so stale auto-close can skip unsafe sessions.
|
|
908
|
+
*/
|
|
909
|
+
export async function getSessionActivityForStaleCheck(specDir, sessionId) {
|
|
910
|
+
const metadata = await getSession(specDir, sessionId);
|
|
911
|
+
if (!metadata) {
|
|
912
|
+
return {
|
|
913
|
+
ok: false,
|
|
914
|
+
reason: "invalid_started_at",
|
|
915
|
+
detail: `Session metadata missing or unreadable for ${sessionId}`,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
const startedAtTs = new Date(metadata.started_at).getTime();
|
|
919
|
+
if (Number.isNaN(startedAtTs)) {
|
|
920
|
+
return {
|
|
921
|
+
ok: false,
|
|
922
|
+
reason: "invalid_started_at",
|
|
923
|
+
detail: `Session ${sessionId} has invalid started_at timestamp`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
927
|
+
let content;
|
|
928
|
+
try {
|
|
929
|
+
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
933
|
+
return {
|
|
934
|
+
ok: true,
|
|
935
|
+
activity: {
|
|
936
|
+
lastActivityAt: metadata.started_at,
|
|
937
|
+
lastActivityTs: startedAtTs,
|
|
938
|
+
source: "started_at",
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
943
|
+
return {
|
|
944
|
+
ok: false,
|
|
945
|
+
reason: "events_unreadable",
|
|
946
|
+
detail: `Unable to read events.jsonl for ${sessionId}: ${message}`,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const lines = content
|
|
950
|
+
.split("\n")
|
|
951
|
+
.map((line) => line.trim())
|
|
952
|
+
.filter((line) => line.length > 0);
|
|
953
|
+
if (lines.length === 0) {
|
|
954
|
+
return {
|
|
955
|
+
ok: true,
|
|
956
|
+
activity: {
|
|
957
|
+
lastActivityAt: metadata.started_at,
|
|
958
|
+
lastActivityTs: startedAtTs,
|
|
959
|
+
source: "started_at",
|
|
960
|
+
},
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
let latestTs = null;
|
|
964
|
+
for (let i = 0; i < lines.length; i++) {
|
|
965
|
+
const line = lines[i];
|
|
966
|
+
try {
|
|
967
|
+
const parsed = JSON.parse(line);
|
|
968
|
+
const event = SessionEventSchema.parse(parsed);
|
|
969
|
+
if (latestTs === null || event.ts > latestTs) {
|
|
970
|
+
latestTs = event.ts;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
return {
|
|
975
|
+
ok: false,
|
|
976
|
+
reason: "events_corrupt",
|
|
977
|
+
detail: `Corrupt events.jsonl for ${sessionId}: invalid line ${i + 1}`,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (latestTs === null) {
|
|
982
|
+
return {
|
|
983
|
+
ok: true,
|
|
984
|
+
activity: {
|
|
985
|
+
lastActivityAt: metadata.started_at,
|
|
986
|
+
lastActivityTs: startedAtTs,
|
|
987
|
+
source: "started_at",
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
ok: true,
|
|
993
|
+
activity: {
|
|
994
|
+
lastActivityAt: new Date(latestTs).toISOString(),
|
|
995
|
+
lastActivityTs: latestTs,
|
|
996
|
+
source: "event",
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
export function evaluateStaleSession(startedAt, activity, criteria, nowMs = Date.now()) {
|
|
1001
|
+
const startedAtMs = new Date(startedAt).getTime();
|
|
1002
|
+
const ageMs = nowMs - startedAtMs;
|
|
1003
|
+
const inactivityMs = nowMs - activity.lastActivityTs;
|
|
1004
|
+
const meetsAgeThreshold = ageMs >= criteria.olderThanMs;
|
|
1005
|
+
const meetsInactivityThreshold = inactivityMs >= criteria.inactiveForMs;
|
|
1006
|
+
const blockedByLivenessGuard = inactivityMs < criteria.livenessGuardMs;
|
|
1007
|
+
const eligible = meetsAgeThreshold &&
|
|
1008
|
+
meetsInactivityThreshold &&
|
|
1009
|
+
!blockedByLivenessGuard;
|
|
1010
|
+
return {
|
|
1011
|
+
startedAt,
|
|
1012
|
+
lastActivityAt: activity.lastActivityAt,
|
|
1013
|
+
lastActivitySource: activity.source,
|
|
1014
|
+
ageMs,
|
|
1015
|
+
inactivityMs,
|
|
1016
|
+
meetsAgeThreshold,
|
|
1017
|
+
meetsInactivityThreshold,
|
|
1018
|
+
blockedByLivenessGuard,
|
|
1019
|
+
eligible,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
export async function selectStaleActiveSessions(specDir, criteriaInput = {}, nowMs = Date.now()) {
|
|
1023
|
+
const criteriaResolved = resolveStaleSessionCriteria(criteriaInput);
|
|
1024
|
+
if (!criteriaResolved.ok) {
|
|
1025
|
+
throw new Error(`${criteriaResolved.message}. ${criteriaResolved.guidance}`);
|
|
1026
|
+
}
|
|
1027
|
+
const criteria = criteriaResolved.criteria;
|
|
1028
|
+
const sessionIds = await listSessions(specDir);
|
|
1029
|
+
const evaluations = [];
|
|
1030
|
+
const candidates = [];
|
|
1031
|
+
const skipped = [];
|
|
1032
|
+
for (const sessionId of sessionIds) {
|
|
1033
|
+
const metadata = await getSession(specDir, sessionId);
|
|
1034
|
+
if (!metadata || metadata.status !== "active")
|
|
1035
|
+
continue;
|
|
1036
|
+
const activityResult = await getSessionActivityForStaleCheck(specDir, sessionId);
|
|
1037
|
+
if (!activityResult.ok) {
|
|
1038
|
+
skipped.push({
|
|
1039
|
+
sessionId,
|
|
1040
|
+
reason: activityResult.reason,
|
|
1041
|
+
detail: activityResult.detail,
|
|
1042
|
+
});
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
const evaluation = evaluateStaleSession(metadata.started_at, activityResult.activity, criteria, nowMs);
|
|
1046
|
+
const withSessionId = {
|
|
1047
|
+
sessionId,
|
|
1048
|
+
...evaluation,
|
|
1049
|
+
};
|
|
1050
|
+
evaluations.push(withSessionId);
|
|
1051
|
+
if (withSessionId.eligible) {
|
|
1052
|
+
candidates.push(withSessionId);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
criteria,
|
|
1057
|
+
totalActiveSessions: evaluations.length + skipped.length,
|
|
1058
|
+
evaluations,
|
|
1059
|
+
candidates,
|
|
1060
|
+
skipped,
|
|
1061
|
+
skippedCount: skipped.length,
|
|
1062
|
+
failureCount: skipped.length,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Build canonical close_reason for stale auto-abandon updates.
|
|
1067
|
+
*
|
|
1068
|
+
* Canonical format:
|
|
1069
|
+
* auto-abandoned:older-than=<duration>,inactive-for=<duration>,liveness-guard=<duration>,last-activity=<iso>
|
|
1070
|
+
*/
|
|
1071
|
+
export function buildAutoAbandonedCloseReason(criteria, evaluation) {
|
|
1072
|
+
const segments = [
|
|
1073
|
+
`older-than=${criteria.olderThan}`,
|
|
1074
|
+
`inactive-for=${criteria.inactiveFor}`,
|
|
1075
|
+
`liveness-guard=${criteria.livenessGuard}`,
|
|
1076
|
+
`last-activity=${evaluation.lastActivityAt}`,
|
|
1077
|
+
];
|
|
1078
|
+
return `auto-abandoned:${segments.join(",")}`;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Apply abandoned metadata to stale session candidates.
|
|
1082
|
+
*
|
|
1083
|
+
* All updates in a single invocation share one ended_at timestamp, which lets
|
|
1084
|
+
* the caller persist and commit the batch atomically with one shadow commit.
|
|
1085
|
+
*/
|
|
1086
|
+
export async function applyAutoAbandonMetadata(specDir, selection, options) {
|
|
1087
|
+
const dryRun = options?.dryRun === true;
|
|
1088
|
+
const endedAt = new Date(options?.nowMs ?? Date.now()).toISOString();
|
|
1089
|
+
const updates = [];
|
|
1090
|
+
for (const candidate of selection.candidates) {
|
|
1091
|
+
const closeReason = buildAutoAbandonedCloseReason(selection.criteria, candidate);
|
|
1092
|
+
updates.push({
|
|
1093
|
+
sessionId: candidate.sessionId,
|
|
1094
|
+
status: "abandoned",
|
|
1095
|
+
endedAt,
|
|
1096
|
+
closeReason,
|
|
1097
|
+
});
|
|
1098
|
+
if (dryRun)
|
|
1099
|
+
continue;
|
|
1100
|
+
const metadata = await getSession(specDir, candidate.sessionId);
|
|
1101
|
+
if (!metadata)
|
|
1102
|
+
continue;
|
|
1103
|
+
const updated = {
|
|
1104
|
+
...metadata,
|
|
1105
|
+
status: "abandoned",
|
|
1106
|
+
ended_at: endedAt,
|
|
1107
|
+
close_reason: closeReason,
|
|
1108
|
+
};
|
|
1109
|
+
const metadataPath = getSessionMetadataPath(specDir, candidate.sessionId);
|
|
1110
|
+
const content = YAML.stringify(updated, {
|
|
1111
|
+
indent: 2,
|
|
1112
|
+
lineWidth: 100,
|
|
1113
|
+
sortMapEntries: false,
|
|
1114
|
+
});
|
|
1115
|
+
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
1116
|
+
}
|
|
1117
|
+
let shadowCommitted;
|
|
1118
|
+
if (!dryRun && updates.length > 0 && options?.shadowCommitMessage) {
|
|
1119
|
+
shadowCommitted = await shadowAutoCommit(specDir, options.shadowCommitMessage);
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
dryRun,
|
|
1123
|
+
updatedCount: updates.length,
|
|
1124
|
+
updates,
|
|
1125
|
+
shadowCommitted,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
// ─── Session Log Summaries ───────────────────────────────────────────────────
|
|
1129
|
+
/**
|
|
1130
|
+
* Determine session type from metadata for display.
|
|
1131
|
+
* - "invocation": New agent runtime session (has trigger != "legacy" or agent_id)
|
|
1132
|
+
* - "loop": Legacy ralph loop session (no trigger, or trigger === "legacy")
|
|
1133
|
+
*
|
|
1134
|
+
* AC: @session-model-evolution ac-6
|
|
1135
|
+
*/
|
|
1136
|
+
function resolveSessionType(metadata) {
|
|
1137
|
+
if (metadata.trigger && metadata.trigger !== "legacy") {
|
|
1138
|
+
return "invocation";
|
|
1139
|
+
}
|
|
1140
|
+
return "loop";
|
|
1141
|
+
}
|
|
768
1142
|
/**
|
|
769
1143
|
* Count lines in events.jsonl without parsing JSON.
|
|
770
1144
|
* Much faster than readEvents() for large files.
|
|
@@ -798,8 +1172,13 @@ async function countIterations(specDir, sessionId) {
|
|
|
798
1172
|
* Count task completions by scanning events for tool calls that invoke
|
|
799
1173
|
* `kspec task complete` or `npm run dev -- task complete`.
|
|
800
1174
|
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
1175
|
+
* Handles multiple adapter formats:
|
|
1176
|
+
* - claude-code-acp: rawInput.command is a string in tool_call_update events
|
|
1177
|
+
* - claude-agent-acp: rawInput.command is a string in tool_call_update events
|
|
1178
|
+
* (initial tool_call event has empty rawInput {})
|
|
1179
|
+
* - codex-acp: rawInput.command is an array ['/usr/bin/bash', '-lc', 'kspec task complete @ref']
|
|
1180
|
+
* in tool_call events; actual command is at index 2
|
|
1181
|
+
*
|
|
803
1182
|
* We use a fast substring check before JSON parsing for performance.
|
|
804
1183
|
*/
|
|
805
1184
|
async function countTaskCompletions(specDir, sessionId) {
|
|
@@ -814,12 +1193,15 @@ async function countTaskCompletions(specDir, sessionId) {
|
|
|
814
1193
|
// Quick substring pre-filter: only parse lines that might contain task complete commands
|
|
815
1194
|
if (!line.includes("task complete"))
|
|
816
1195
|
continue;
|
|
817
|
-
// Must be a tool_call event (session.update with sessionUpdate: "tool_call")
|
|
818
|
-
if (!line.includes('"tool_call"'))
|
|
819
|
-
continue;
|
|
820
1196
|
try {
|
|
821
1197
|
const event = JSON.parse(line);
|
|
822
|
-
const
|
|
1198
|
+
const rawCommand = event?.data?.update?.rawInput?.command;
|
|
1199
|
+
// Normalize: string (claude-*-acp) or array like [bash, -lc, cmd] (codex-acp)
|
|
1200
|
+
const command = typeof rawCommand === "string"
|
|
1201
|
+
? rawCommand
|
|
1202
|
+
: Array.isArray(rawCommand) && rawCommand.length > 0
|
|
1203
|
+
? rawCommand[rawCommand.length - 1]
|
|
1204
|
+
: undefined;
|
|
823
1205
|
if (typeof command === "string" && /\btask complete\b/.test(command)) {
|
|
824
1206
|
count++;
|
|
825
1207
|
}
|
|
@@ -861,6 +1243,7 @@ export async function getSessionLogSummary(specDir, sessionId) {
|
|
|
861
1243
|
id: metadata.id,
|
|
862
1244
|
status: metadata.status,
|
|
863
1245
|
agent_type: metadata.agent_type,
|
|
1246
|
+
session_type: resolveSessionType(metadata),
|
|
864
1247
|
started_at: metadata.started_at,
|
|
865
1248
|
ended_at: metadata.ended_at,
|
|
866
1249
|
duration_ms: durationMs,
|
|
@@ -1015,7 +1398,13 @@ function extractTaskTransitions(events) {
|
|
|
1015
1398
|
for (const event of events) {
|
|
1016
1399
|
if (event.type === "session.update") {
|
|
1017
1400
|
const data = event.data;
|
|
1018
|
-
const
|
|
1401
|
+
const rawCommand = data?.update?.rawInput?.command;
|
|
1402
|
+
// Normalize: string (claude-*-acp) or array like [bash, -lc, cmd] (codex-acp)
|
|
1403
|
+
const command = typeof rawCommand === "string"
|
|
1404
|
+
? rawCommand
|
|
1405
|
+
: Array.isArray(rawCommand) && rawCommand.length > 0
|
|
1406
|
+
? rawCommand[rawCommand.length - 1]
|
|
1407
|
+
: undefined;
|
|
1019
1408
|
if (typeof command === "string") {
|
|
1020
1409
|
if (/\btask start\b/.test(command)) {
|
|
1021
1410
|
const ref = extractTaskRef(command);
|
|
@@ -1194,6 +1583,7 @@ export async function getSessionLogDetail(specDir, sessionId) {
|
|
|
1194
1583
|
id: metadata.id,
|
|
1195
1584
|
status: metadata.status,
|
|
1196
1585
|
agent_type: metadata.agent_type,
|
|
1586
|
+
session_type: resolveSessionType(metadata),
|
|
1197
1587
|
task_id: metadata.task_id,
|
|
1198
1588
|
started_at: metadata.started_at,
|
|
1199
1589
|
ended_at: metadata.ended_at,
|
|
@@ -1232,6 +1622,8 @@ export function computeSessionLogStats(summaries) {
|
|
|
1232
1622
|
active: 0,
|
|
1233
1623
|
completed: 0,
|
|
1234
1624
|
abandoned: 0,
|
|
1625
|
+
timed_out: 0,
|
|
1626
|
+
failed: 0,
|
|
1235
1627
|
};
|
|
1236
1628
|
for (const s of summaries) {
|
|
1237
1629
|
totalEvents += s.event_count;
|
|
@@ -1243,7 +1635,7 @@ export function computeSessionLogStats(summaries) {
|
|
|
1243
1635
|
const n = summaries.length;
|
|
1244
1636
|
// Build status breakdown
|
|
1245
1637
|
const statusBreakdown = [];
|
|
1246
|
-
for (const status of ["completed", "active", "abandoned"]) {
|
|
1638
|
+
for (const status of ["completed", "active", "abandoned", "timed_out", "failed"]) {
|
|
1247
1639
|
const count = statusCounts[status] || 0;
|
|
1248
1640
|
if (count > 0) {
|
|
1249
1641
|
statusBreakdown.push({
|