@gajae-code/coding-agent 0.18.6 → 0.18.7
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/CHANGELOG.md +39 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/router/session-router.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
- package/dist/types/session/session-retirement.d.ts +47 -0
- package/dist/types/session/session-storage.d.ts +0 -46
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/package.json +7 -7
- package/scripts/resident-memory-bench.ts +174 -20
- package/src/capability/index.ts +1 -0
- package/src/capability/types.ts +4 -0
- package/src/cli/config-cli.ts +8 -18
- package/src/cli-main.ts +7 -8
- package/src/commands/sdk.ts +112 -21
- package/src/config/settings.ts +44 -4
- package/src/discovery/helpers.ts +9 -3
- package/src/discovery/ssh.ts +8 -3
- package/src/gjc-runtime/gc-runtime.ts +2 -1
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/acp/acp-agent.ts +75 -3
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/sdk/broker/broker.ts +8 -1
- package/src/sdk/broker/lifecycle.ts +109 -7
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/session-runtime.test.ts +43 -2
- package/src/sdk/host/session-runtime.ts +2 -2
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/router/session-router.ts +19 -2
- package/src/sdk/session.ts +14 -0
- package/src/session/agent-session.ts +57 -6
- package/src/session/internal/managed-session-storage.ts +107 -1
- package/src/session/internal/task-artifact-owner-access.ts +229 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
- package/src/session/session-retirement.ts +155 -0
- package/src/session/session-storage.ts +1 -160
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +578 -0
- package/src/session/task-artifact-owner-retirement.ts +475 -0
- package/src/session/task-artifact-owner.ts +289 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { toError } from "@gajae-code/utils";
|
|
5
|
+
import {
|
|
6
|
+
type FileSessionStorage,
|
|
7
|
+
SessionDeleteVerificationError,
|
|
8
|
+
type SessionStorageSnapshot,
|
|
9
|
+
type VerifiedSessionDeleteTarget,
|
|
10
|
+
} from "./session-storage";
|
|
11
|
+
import { parseFirstJsonlLine } from "./session-transcript-header";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Outcome of a disk-retention retirement attempt.
|
|
15
|
+
*
|
|
16
|
+
* `kept` means the transcript survived and nothing of the session was
|
|
17
|
+
* destroyed, with the exact reason so `gjc gc --disk` can report it.
|
|
18
|
+
* `cleanup_pending` means the delete authority already detached or removed the
|
|
19
|
+
* session's artifact tree (or quarantined the transcript) before it stopped:
|
|
20
|
+
* the record survives, the session does not, and a caller must NOT report that
|
|
21
|
+
* as an ordinary keep.
|
|
22
|
+
*/
|
|
23
|
+
export type SessionRetirementOutcome =
|
|
24
|
+
| { kind: "retired" }
|
|
25
|
+
| { kind: "kept"; reason: string }
|
|
26
|
+
| { kind: "cleanup_pending"; reason: string };
|
|
27
|
+
|
|
28
|
+
/** A retirement that cleared every precondition the retention pass itself owns. */
|
|
29
|
+
type SessionRetirementPlan =
|
|
30
|
+
| { kind: "retirable"; target: VerifiedSessionDeleteTarget; artifactsPresent: boolean }
|
|
31
|
+
| { kind: "kept"; reason: string };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate the preconditions retirement owns — the transcript must be readable
|
|
35
|
+
* and carry a usable session header — and bind the delete target to the exact
|
|
36
|
+
* bytes just read. Nothing here mutates the store.
|
|
37
|
+
*/
|
|
38
|
+
function planSessionRetirement(
|
|
39
|
+
storage: FileSessionStorage,
|
|
40
|
+
sessionsRoot: string,
|
|
41
|
+
transcriptPath: string,
|
|
42
|
+
): SessionRetirementPlan {
|
|
43
|
+
let snapshot: SessionStorageSnapshot;
|
|
44
|
+
try {
|
|
45
|
+
snapshot = storage.readSnapshotSync(transcriptPath);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { kind: "kept", reason: `transcript_unreadable: ${toError(err).message}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const header = parseFirstJsonlLine(snapshot.bytes);
|
|
51
|
+
if (header?.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string") {
|
|
52
|
+
return { kind: "kept", reason: "transcript_header_unusable" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The verified delete authority only unlinks a single-link transcript, so a
|
|
56
|
+
// hard-linked one can never be retired. Refusing here keeps the dry run from
|
|
57
|
+
// promising bytes prune cannot release, and — because this runs before the
|
|
58
|
+
// artifact phase — keeps a doomed retirement from destroying the artifact
|
|
59
|
+
// tree of a session whose transcript will survive anyway.
|
|
60
|
+
if (snapshot.stat.nlink === undefined || snapshot.stat.nlink !== 1n) {
|
|
61
|
+
return { kind: "kept", reason: "transcript_not_single_link" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const directory = path.dirname(transcriptPath);
|
|
65
|
+
return {
|
|
66
|
+
kind: "retirable",
|
|
67
|
+
artifactsPresent: storage.existsSync(transcriptPath.slice(0, -".jsonl".length)),
|
|
68
|
+
target: {
|
|
69
|
+
sessionsRoot,
|
|
70
|
+
transcriptPath,
|
|
71
|
+
sessionId: header.id,
|
|
72
|
+
cwd: header.cwd,
|
|
73
|
+
transcriptIdentity: {
|
|
74
|
+
dev: snapshot.stat.dev,
|
|
75
|
+
ino: snapshot.stat.ino,
|
|
76
|
+
nlink: snapshot.stat.nlink,
|
|
77
|
+
size: snapshot.stat.size,
|
|
78
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
79
|
+
sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
80
|
+
},
|
|
81
|
+
plannedArtifactsPath: path.join(directory, `.gjc-delete-gc-${crypto.randomUUID()}-artifacts`),
|
|
82
|
+
plannedTranscriptPath: path.join(directory, `.gjc-delete-gc-${crypto.randomUUID()}-transcript`),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Non-mutating projection of {@link retireSessionTranscript}: would the
|
|
89
|
+
* retention pass's own preconditions let this transcript be retired at all?
|
|
90
|
+
*
|
|
91
|
+
* `gjc gc --disk` runs it so a dry run reports the verdict a prune would reach
|
|
92
|
+
* instead of promising bytes the delete authority will refuse to release. The
|
|
93
|
+
* authority's verdict (containment, identity, artifact tree) is deliberately
|
|
94
|
+
* not predicted here — it is re-derived against live state at delete time.
|
|
95
|
+
*/
|
|
96
|
+
export function probeSessionRetirement(
|
|
97
|
+
storage: FileSessionStorage,
|
|
98
|
+
sessionsRoot: string,
|
|
99
|
+
transcriptPath: string,
|
|
100
|
+
): { kind: "retirable" } | { kind: "kept"; reason: string } {
|
|
101
|
+
const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
|
|
102
|
+
return plan.kind === "retirable" ? { kind: "retirable" } : plan;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A transcript that survives after its artifacts are already gone is not a
|
|
107
|
+
* benign keep: the session is half-destroyed and the caller has to surface it.
|
|
108
|
+
*/
|
|
109
|
+
function retirementIncomplete(artifactsRemoved: boolean, reason: string): SessionRetirementOutcome {
|
|
110
|
+
return artifactsRemoved ? { kind: "cleanup_pending", reason } : { kind: "kept", reason };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Retire one managed session transcript through the verified hard-delete
|
|
115
|
+
* authority.
|
|
116
|
+
*
|
|
117
|
+
* This is the only supported way for a retention pass to remove a transcript:
|
|
118
|
+
* identity, containment, header id/cwd, artifact tree and parent directory are
|
|
119
|
+
* all re-verified inside {@link FileSessionStorage.deleteSessionVerified}, and
|
|
120
|
+
* anything ambiguous fails closed as `kept` rather than deleting bytes. The
|
|
121
|
+
* caller owns the retention policy; this function owns nothing but the delete
|
|
122
|
+
* authority.
|
|
123
|
+
*/
|
|
124
|
+
export async function retireSessionTranscript(
|
|
125
|
+
storage: FileSessionStorage,
|
|
126
|
+
sessionsRoot: string,
|
|
127
|
+
transcriptPath: string,
|
|
128
|
+
): Promise<SessionRetirementOutcome> {
|
|
129
|
+
const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
|
|
130
|
+
if (plan.kind === "kept") return plan;
|
|
131
|
+
|
|
132
|
+
let artifactsRemoved = false;
|
|
133
|
+
try {
|
|
134
|
+
// Phase 1 removes the sibling artifact directory (or proves it absent);
|
|
135
|
+
// phase 2 unlinks the transcript only against the same bound identity.
|
|
136
|
+
const artifacts = await storage.deleteSessionVerified(plan.target);
|
|
137
|
+
if (artifacts.kind === "deleted") return { kind: "retired" };
|
|
138
|
+
if (artifacts.kind === "cleanup_pending") {
|
|
139
|
+
return { kind: "cleanup_pending", reason: `cleanup_pending_${artifacts.phase}: ${artifacts.error.message}` };
|
|
140
|
+
}
|
|
141
|
+
// Phase 1 is durable: whatever artifact tree the session had is gone, so
|
|
142
|
+
// every later refusal leaves a record without its artifacts.
|
|
143
|
+
artifactsRemoved = plan.artifactsPresent;
|
|
144
|
+
const deletion = await storage.deleteSessionVerified({ ...plan.target, artifactsRemoved: true });
|
|
145
|
+
if (deletion.kind === "deleted") return { kind: "retired" };
|
|
146
|
+
if (deletion.kind === "cleanup_pending") {
|
|
147
|
+
return { kind: "cleanup_pending", reason: `cleanup_pending_${deletion.phase}: ${deletion.error.message}` };
|
|
148
|
+
}
|
|
149
|
+
return retirementIncomplete(artifactsRemoved, "transcript_not_deleted");
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const error = toError(err);
|
|
152
|
+
const kind = err instanceof SessionDeleteVerificationError ? err.kind : "error";
|
|
153
|
+
return retirementIncomplete(artifactsRemoved, `verified_delete_rejected(${kind}): ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type NativePublishOutcome,
|
|
29
29
|
} from "./internal/native-publish-outcome";
|
|
30
30
|
import { isDerivedSessionMemoryFile } from "./internal/session-memory-sidecar";
|
|
31
|
+
import { parseFirstJsonlLine } from "./session-transcript-header";
|
|
31
32
|
|
|
32
33
|
const utf8Decoder = new TextDecoder("utf-8");
|
|
33
34
|
const newlineBuffer = Buffer.from("\n", "utf8");
|
|
@@ -2883,22 +2884,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
2883
2884
|
}
|
|
2884
2885
|
}
|
|
2885
2886
|
|
|
2886
|
-
/** Parse the first JSONL line as a generic record; returns undefined on parse failure. */
|
|
2887
|
-
function parseFirstJsonlLine(bytes: Uint8Array): Record<string, unknown> | undefined {
|
|
2888
|
-
const NL = 0x0a;
|
|
2889
|
-
const end = bytes.indexOf(NL);
|
|
2890
|
-
const firstLine = end === -1 ? bytes : bytes.subarray(0, end);
|
|
2891
|
-
if (firstLine.length === 0) return undefined;
|
|
2892
|
-
try {
|
|
2893
|
-
const text = utf8Decoder.decode(firstLine).trim();
|
|
2894
|
-
if (!text) return undefined;
|
|
2895
|
-
const value: unknown = JSON.parse(text);
|
|
2896
|
-
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
2897
|
-
} catch {
|
|
2898
|
-
return undefined;
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
2887
|
function matchesPattern(name: string, pattern: string): boolean {
|
|
2903
2888
|
if (pattern === "*") return true;
|
|
2904
2889
|
if (pattern === ".*") return name.startsWith(".");
|
|
@@ -3531,147 +3516,3 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
3531
3516
|
return new MemoryStagedStreamingWriter(this, path);
|
|
3532
3517
|
}
|
|
3533
3518
|
}
|
|
3534
|
-
|
|
3535
|
-
/**
|
|
3536
|
-
* Outcome of a disk-retention retirement attempt.
|
|
3537
|
-
*
|
|
3538
|
-
* `kept` means the transcript survived and nothing of the session was
|
|
3539
|
-
* destroyed, with the exact reason so `gjc gc --disk` can report it.
|
|
3540
|
-
* `cleanup_pending` means the delete authority already detached or removed the
|
|
3541
|
-
* session's artifact tree (or quarantined the transcript) before it stopped:
|
|
3542
|
-
* the record survives, the session does not, and a caller must NOT report that
|
|
3543
|
-
* as an ordinary keep.
|
|
3544
|
-
*/
|
|
3545
|
-
export type SessionRetirementOutcome =
|
|
3546
|
-
| { kind: "retired" }
|
|
3547
|
-
| { kind: "kept"; reason: string }
|
|
3548
|
-
| { kind: "cleanup_pending"; reason: string };
|
|
3549
|
-
|
|
3550
|
-
/** A retirement that cleared every precondition the retention pass itself owns. */
|
|
3551
|
-
type SessionRetirementPlan =
|
|
3552
|
-
| { kind: "retirable"; target: VerifiedSessionDeleteTarget; artifactsPresent: boolean }
|
|
3553
|
-
| { kind: "kept"; reason: string };
|
|
3554
|
-
|
|
3555
|
-
/**
|
|
3556
|
-
* Evaluate the preconditions retirement owns — the transcript must be readable
|
|
3557
|
-
* and carry a usable session header — and bind the delete target to the exact
|
|
3558
|
-
* bytes just read. Nothing here mutates the store.
|
|
3559
|
-
*/
|
|
3560
|
-
function planSessionRetirement(
|
|
3561
|
-
storage: FileSessionStorage,
|
|
3562
|
-
sessionsRoot: string,
|
|
3563
|
-
transcriptPath: string,
|
|
3564
|
-
): SessionRetirementPlan {
|
|
3565
|
-
let snapshot: SessionStorageSnapshot;
|
|
3566
|
-
try {
|
|
3567
|
-
snapshot = storage.readSnapshotSync(transcriptPath);
|
|
3568
|
-
} catch (err) {
|
|
3569
|
-
return { kind: "kept", reason: `transcript_unreadable: ${toError(err).message}` };
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
const header = parseFirstJsonlLine(snapshot.bytes);
|
|
3573
|
-
if (header?.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string") {
|
|
3574
|
-
return { kind: "kept", reason: "transcript_header_unusable" };
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
// The verified delete authority only unlinks a single-link transcript, so a
|
|
3578
|
-
// hard-linked one can never be retired. Refusing here keeps the dry run from
|
|
3579
|
-
// promising bytes prune cannot release, and — because this runs before the
|
|
3580
|
-
// artifact phase — keeps a doomed retirement from destroying the artifact
|
|
3581
|
-
// tree of a session whose transcript will survive anyway.
|
|
3582
|
-
if (snapshot.stat.nlink === undefined || snapshot.stat.nlink !== 1n) {
|
|
3583
|
-
return { kind: "kept", reason: "transcript_not_single_link" };
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
const directory = path.dirname(transcriptPath);
|
|
3587
|
-
return {
|
|
3588
|
-
kind: "retirable",
|
|
3589
|
-
artifactsPresent: storage.existsSync(transcriptPath.slice(0, -".jsonl".length)),
|
|
3590
|
-
target: {
|
|
3591
|
-
sessionsRoot,
|
|
3592
|
-
transcriptPath,
|
|
3593
|
-
sessionId: header.id,
|
|
3594
|
-
cwd: header.cwd,
|
|
3595
|
-
transcriptIdentity: {
|
|
3596
|
-
dev: snapshot.stat.dev,
|
|
3597
|
-
ino: snapshot.stat.ino,
|
|
3598
|
-
nlink: snapshot.stat.nlink,
|
|
3599
|
-
size: snapshot.stat.size,
|
|
3600
|
-
mtimeNs: snapshot.stat.mtimeNs,
|
|
3601
|
-
sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
3602
|
-
},
|
|
3603
|
-
plannedArtifactsPath: path.join(directory, `.gjc-delete-gc-${randomUUID()}-artifacts`),
|
|
3604
|
-
plannedTranscriptPath: path.join(directory, `.gjc-delete-gc-${randomUUID()}-transcript`),
|
|
3605
|
-
},
|
|
3606
|
-
};
|
|
3607
|
-
}
|
|
3608
|
-
|
|
3609
|
-
/**
|
|
3610
|
-
* Non-mutating projection of {@link retireSessionTranscript}: would the
|
|
3611
|
-
* retention pass's own preconditions let this transcript be retired at all?
|
|
3612
|
-
*
|
|
3613
|
-
* `gjc gc --disk` runs it so a dry run reports the verdict a prune would reach
|
|
3614
|
-
* instead of promising bytes the delete authority will refuse to release. The
|
|
3615
|
-
* authority's verdict (containment, identity, artifact tree) is deliberately
|
|
3616
|
-
* not predicted here — it is re-derived against live state at delete time.
|
|
3617
|
-
*/
|
|
3618
|
-
export function probeSessionRetirement(
|
|
3619
|
-
storage: FileSessionStorage,
|
|
3620
|
-
sessionsRoot: string,
|
|
3621
|
-
transcriptPath: string,
|
|
3622
|
-
): { kind: "retirable" } | { kind: "kept"; reason: string } {
|
|
3623
|
-
const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
|
|
3624
|
-
return plan.kind === "retirable" ? { kind: "retirable" } : plan;
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
/**
|
|
3628
|
-
* A transcript that survives after its artifacts are already gone is not a
|
|
3629
|
-
* benign keep: the session is half-destroyed and the caller has to surface it.
|
|
3630
|
-
*/
|
|
3631
|
-
function retirementIncomplete(artifactsRemoved: boolean, reason: string): SessionRetirementOutcome {
|
|
3632
|
-
return artifactsRemoved ? { kind: "cleanup_pending", reason } : { kind: "kept", reason };
|
|
3633
|
-
}
|
|
3634
|
-
|
|
3635
|
-
/**
|
|
3636
|
-
* Retire one managed session transcript through the verified hard-delete
|
|
3637
|
-
* authority.
|
|
3638
|
-
*
|
|
3639
|
-
* This is the only supported way for a retention pass to remove a transcript:
|
|
3640
|
-
* identity, containment, header id/cwd, artifact tree and parent directory are
|
|
3641
|
-
* all re-verified inside {@link FileSessionStorage.deleteSessionVerified}, and
|
|
3642
|
-
* anything ambiguous fails closed as `kept` rather than deleting bytes. The
|
|
3643
|
-
* caller owns the retention policy; this function owns nothing but the delete
|
|
3644
|
-
* authority.
|
|
3645
|
-
*/
|
|
3646
|
-
export async function retireSessionTranscript(
|
|
3647
|
-
storage: FileSessionStorage,
|
|
3648
|
-
sessionsRoot: string,
|
|
3649
|
-
transcriptPath: string,
|
|
3650
|
-
): Promise<SessionRetirementOutcome> {
|
|
3651
|
-
const plan = planSessionRetirement(storage, sessionsRoot, transcriptPath);
|
|
3652
|
-
if (plan.kind === "kept") return plan;
|
|
3653
|
-
|
|
3654
|
-
let artifactsRemoved = false;
|
|
3655
|
-
try {
|
|
3656
|
-
// Phase 1 removes the sibling artifact directory (or proves it absent);
|
|
3657
|
-
// phase 2 unlinks the transcript only against the same bound identity.
|
|
3658
|
-
const artifacts = await storage.deleteSessionVerified(plan.target);
|
|
3659
|
-
if (artifacts.kind === "deleted") return { kind: "retired" };
|
|
3660
|
-
if (artifacts.kind === "cleanup_pending") {
|
|
3661
|
-
return { kind: "cleanup_pending", reason: `cleanup_pending_${artifacts.phase}: ${artifacts.error.message}` };
|
|
3662
|
-
}
|
|
3663
|
-
// Phase 1 is durable: whatever artifact tree the session had is gone, so
|
|
3664
|
-
// every later refusal leaves a record without its artifacts.
|
|
3665
|
-
artifactsRemoved = plan.artifactsPresent;
|
|
3666
|
-
const deletion = await storage.deleteSessionVerified({ ...plan.target, artifactsRemoved: true });
|
|
3667
|
-
if (deletion.kind === "deleted") return { kind: "retired" };
|
|
3668
|
-
if (deletion.kind === "cleanup_pending") {
|
|
3669
|
-
return { kind: "cleanup_pending", reason: `cleanup_pending_${deletion.phase}: ${deletion.error.message}` };
|
|
3670
|
-
}
|
|
3671
|
-
return retirementIncomplete(artifactsRemoved, "transcript_not_deleted");
|
|
3672
|
-
} catch (err) {
|
|
3673
|
-
const error = toError(err);
|
|
3674
|
-
const kind = err instanceof SessionDeleteVerificationError ? err.kind : "error";
|
|
3675
|
-
return retirementIncomplete(artifactsRemoved, `verified_delete_rejected(${kind}): ${error.message}`);
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const utf8Decoder = new TextDecoder("utf-8");
|
|
2
|
+
|
|
3
|
+
/** Parse the first JSONL line as a generic record; returns undefined on parse failure. */
|
|
4
|
+
export function parseFirstJsonlLine(bytes: Uint8Array): Record<string, unknown> | undefined {
|
|
5
|
+
const NL = 0x0a;
|
|
6
|
+
const end = bytes.indexOf(NL);
|
|
7
|
+
const firstLine = end === -1 ? bytes : bytes.subarray(0, end);
|
|
8
|
+
if (firstLine.length === 0) return undefined;
|
|
9
|
+
try {
|
|
10
|
+
const text = utf8Decoder.decode(firstLine).trim();
|
|
11
|
+
if (!text) return undefined;
|
|
12
|
+
const value: unknown = JSON.parse(text);
|
|
13
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|