@giovannijecha/jecode 0.8.4 → 0.8.5
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 +8 -6
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch.js +37 -4
- package/dist/bounded-file.js +212 -0
- package/dist/commands.js +2 -0
- package/dist/config.js +2 -1
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +22 -6
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +13 -2
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +11 -5
- package/dist/provider-errors.js +37 -4
- package/dist/provider-label.js +13 -0
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -19
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +82 -23
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +31 -19
- package/dist/providers/openai-codex.js +67 -41
- package/dist/providers/openai-stream.js +26 -2
- package/dist/providers/openai.js +51 -24
- package/dist/providers/sse.js +52 -8
- package/dist/request-identity.js +32 -0
- package/dist/sessions/lease.js +132 -49
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/store.js +366 -142
- package/dist/settings.js +62 -10
- package/dist/stable-directory.js +148 -0
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- package/dist/tools/fs.js +124 -102
- package/dist/tools/search.js +65 -100
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +32 -4
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/feedback.js +4 -0
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +3 -4
- package/dist/tools/ripgrep.js +0 -230
package/dist/sessions/lease.js
CHANGED
|
@@ -1,83 +1,166 @@
|
|
|
1
|
-
// A tiny,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
// A tiny, generation-safe process lease for one durable conversation.
|
|
2
|
+
import { lstat, unlink } from "node:fs/promises";
|
|
3
|
+
import { BoundedFileError, readBoundedText, stableFileExpectation, } from "../bounded-file.js";
|
|
4
|
+
import { fileIdentity, sameFileIdentity } from "../file-identity.js";
|
|
5
|
+
import { createProcessLeaseDirectory, inspectProcessLease, pidIsAlive, ProcessLeaseError, processLease, processLeaseToken, removeProcessLease, tryAcquireProcessLease, } from "../process-lease.js";
|
|
6
|
+
const LEGACY_LEASE_BYTES = 256;
|
|
7
|
+
const sessionLeaseScopes = new WeakMap();
|
|
5
8
|
export function leaseToken() {
|
|
6
|
-
return
|
|
9
|
+
return processLeaseToken();
|
|
7
10
|
}
|
|
8
|
-
export function
|
|
11
|
+
export async function createLeaseDirectory(directory, token) {
|
|
12
|
+
return createProcessLeaseDirectory(directory, token);
|
|
13
|
+
}
|
|
14
|
+
export async function claimLeaseDirectory(directory, token) {
|
|
15
|
+
try {
|
|
16
|
+
return await tryAcquireProcessLease(directory, token, { staleMs: 0, setupGraceMs: 1_000 });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof ProcessLeaseError)
|
|
20
|
+
throw unsafeLease();
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function leaseFromGeneration(directory, generation) {
|
|
25
|
+
return processLease(directory, generation);
|
|
26
|
+
}
|
|
27
|
+
export function sessionLease(id, scope, lease) {
|
|
9
28
|
let closed = false;
|
|
10
|
-
|
|
29
|
+
const owned = Object.freeze({
|
|
11
30
|
id,
|
|
12
31
|
assertOwned: async () => {
|
|
13
32
|
if (closed)
|
|
14
33
|
throw new Error("session lease is closed");
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
try {
|
|
35
|
+
await lease.assertOwned();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
17
38
|
throw new Error("session lease is no longer owned by this process");
|
|
39
|
+
}
|
|
18
40
|
},
|
|
19
41
|
close: async () => {
|
|
20
42
|
if (closed)
|
|
21
43
|
return;
|
|
44
|
+
const removed = await lease.release();
|
|
45
|
+
if (!removed)
|
|
46
|
+
throw new Error("session lease is no longer owned by this process");
|
|
22
47
|
closed = true;
|
|
23
|
-
try {
|
|
24
|
-
await removeLease(file, token);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// A recovered, replaced, or already-closed lease is no longer ours.
|
|
28
|
-
}
|
|
29
48
|
},
|
|
30
49
|
});
|
|
50
|
+
sessionLeaseScopes.set(owned, { id, scope });
|
|
51
|
+
return owned;
|
|
52
|
+
}
|
|
53
|
+
export function sessionLeaseOwns(lease, id, scope) {
|
|
54
|
+
const owner = sessionLeaseScopes.get(lease);
|
|
55
|
+
return owner?.id === id && owner.scope === scope;
|
|
31
56
|
}
|
|
32
|
-
export async function leaseOwner(
|
|
57
|
+
export async function leaseOwner(directory, hooks = {}) {
|
|
33
58
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const pid = Number(match[1]);
|
|
39
|
-
return { pid: Number.isSafeInteger(pid) ? pid : 0, token };
|
|
59
|
+
const owner = await inspectProcessLease(directory);
|
|
60
|
+
return owner === undefined
|
|
61
|
+
? undefined
|
|
62
|
+
: { pid: owner.pid, token: owner.token, legacy: false };
|
|
40
63
|
}
|
|
41
64
|
catch (error) {
|
|
42
|
-
if (error
|
|
43
|
-
|
|
65
|
+
if (error instanceof ProcessLeaseError) {
|
|
66
|
+
let legacy;
|
|
67
|
+
try {
|
|
68
|
+
legacy = await legacyLeaseOwner(directory, hooks);
|
|
69
|
+
}
|
|
70
|
+
catch (legacyError) {
|
|
71
|
+
if (unsafeLegacyRace(legacyError))
|
|
72
|
+
throw unsafeLease();
|
|
73
|
+
throw legacyError;
|
|
74
|
+
}
|
|
75
|
+
if (legacy !== undefined)
|
|
76
|
+
return legacy;
|
|
77
|
+
throw unsafeLease();
|
|
78
|
+
}
|
|
44
79
|
throw error;
|
|
45
80
|
}
|
|
46
81
|
}
|
|
47
|
-
export async function removeLease(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
export async function removeLease(directory, token) {
|
|
83
|
+
try {
|
|
84
|
+
return await removeProcessLease(directory, token);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof ProcessLeaseError)
|
|
88
|
+
throw unsafeLease();
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
51
91
|
}
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
/** Remove a legacy fixed-file marker only while its session is exclusively owned. */
|
|
93
|
+
export async function removeLegacyLeaseExclusive(file, token, owner) {
|
|
94
|
+
if (!sessionLeaseScopes.has(owner)) {
|
|
95
|
+
throw new Error("legacy lease migration requires a Jecode session lease");
|
|
96
|
+
}
|
|
97
|
+
await owner.assertOwned();
|
|
98
|
+
let before;
|
|
99
|
+
try {
|
|
100
|
+
before = await lstat(file, { bigint: true });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code === "ENOENT")
|
|
104
|
+
return true;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
if (before.isSymbolicLink() || !before.isFile() || before.size > BigInt(LEGACY_LEASE_BYTES)) {
|
|
54
108
|
return false;
|
|
109
|
+
}
|
|
55
110
|
try {
|
|
56
|
-
|
|
111
|
+
const current = await readBoundedText(file, LEGACY_LEASE_BYTES, {
|
|
112
|
+
label: "legacy session lease",
|
|
113
|
+
expected: stableFileExpectation(before),
|
|
114
|
+
});
|
|
115
|
+
if (current !== token)
|
|
116
|
+
return false;
|
|
117
|
+
const after = await lstat(file, { bigint: true });
|
|
118
|
+
if (!sameFileIdentity(fileIdentity(before), fileIdentity(after)))
|
|
119
|
+
return false;
|
|
120
|
+
await owner.assertOwned();
|
|
121
|
+
await unlink(file);
|
|
57
122
|
return true;
|
|
58
123
|
}
|
|
59
124
|
catch (error) {
|
|
60
|
-
|
|
125
|
+
if (unsafeLegacyRace(error))
|
|
126
|
+
return false;
|
|
127
|
+
throw error;
|
|
61
128
|
}
|
|
62
129
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
130
|
+
export { pidIsAlive };
|
|
131
|
+
function unsafeLease() {
|
|
132
|
+
return new Error("session lease is unsafe or too large, or invalid");
|
|
133
|
+
}
|
|
134
|
+
async function legacyLeaseOwner(file, hooks) {
|
|
135
|
+
let details;
|
|
69
136
|
try {
|
|
70
|
-
|
|
71
|
-
if (!opened.isFile() || opened.size > MAX_LEASE_BYTES ||
|
|
72
|
-
opened.dev !== before.dev || opened.ino !== before.ino)
|
|
73
|
-
throw new Error("session lease changed while opening");
|
|
74
|
-
const bytes = Buffer.alloc(MAX_LEASE_BYTES + 1);
|
|
75
|
-
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
76
|
-
if (bytesRead > MAX_LEASE_BYTES)
|
|
77
|
-
throw new Error("session lease is too large");
|
|
78
|
-
return bytes.subarray(0, bytesRead).toString("utf8");
|
|
137
|
+
details = await lstat(file, { bigint: true });
|
|
79
138
|
}
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error.code === "ENOENT")
|
|
141
|
+
return undefined;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if (details.isSymbolicLink() || !details.isFile() || details.size > BigInt(LEGACY_LEASE_BYTES)) {
|
|
145
|
+
return undefined;
|
|
82
146
|
}
|
|
147
|
+
await hooks.afterLegacyStat?.();
|
|
148
|
+
const token = await readBoundedText(file, LEGACY_LEASE_BYTES, {
|
|
149
|
+
label: "legacy session lease",
|
|
150
|
+
expected: stableFileExpectation(details),
|
|
151
|
+
});
|
|
152
|
+
const match = /^([1-9]\d*):/.exec(token.trim());
|
|
153
|
+
const pid = match === null ? 0 : Number(match[1]);
|
|
154
|
+
return {
|
|
155
|
+
pid: Number.isSafeInteger(pid) && pid <= 0x7fff_ffff ? pid : 0,
|
|
156
|
+
token,
|
|
157
|
+
legacy: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function unsafeLegacyRace(error) {
|
|
161
|
+
if (error instanceof BoundedFileError)
|
|
162
|
+
return true;
|
|
163
|
+
const code = error.code;
|
|
164
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" ||
|
|
165
|
+
code === "EISDIR" || code === "ENXIO";
|
|
83
166
|
}
|
package/dist/sessions/runtime.js
CHANGED
|
@@ -3,36 +3,38 @@
|
|
|
3
3
|
// One logical conversation keeps one stable session id across every resume.
|
|
4
4
|
// New turns advance that session's tree head; /new is the boundary that starts
|
|
5
5
|
// another durable session.
|
|
6
|
-
import { DurableSessionStore } from "./store.js";
|
|
6
|
+
import { DurableSessionStore, reserveSessionId } from "./store.js";
|
|
7
7
|
export class SessionPersistence {
|
|
8
8
|
#store;
|
|
9
9
|
#sessionId;
|
|
10
|
+
#conversationId;
|
|
10
11
|
#lease;
|
|
11
12
|
#snapshot;
|
|
12
13
|
#failure;
|
|
13
|
-
constructor(store, sessionId, lease, snapshot) {
|
|
14
|
+
constructor(store, sessionId, lease, snapshot, conversationId = sessionId ?? reserveSessionId()) {
|
|
14
15
|
this.#store = store;
|
|
15
16
|
this.#sessionId = sessionId;
|
|
17
|
+
this.#conversationId = conversationId;
|
|
16
18
|
this.#lease = lease;
|
|
17
19
|
this.#snapshot = snapshot;
|
|
18
20
|
}
|
|
19
21
|
static fresh(store) {
|
|
20
|
-
return new SessionPersistence(store, null);
|
|
22
|
+
return new SessionPersistence(store, null, undefined, undefined, reserveSessionId());
|
|
21
23
|
}
|
|
22
24
|
static async resume(store, id) {
|
|
23
25
|
const lease = await store.claim(id);
|
|
24
26
|
try {
|
|
25
|
-
const snapshot = await store.load(id);
|
|
27
|
+
const snapshot = await store.load(id, lease);
|
|
26
28
|
const conversation = snapshot.conversation.latestResumable();
|
|
27
29
|
if (conversation === undefined)
|
|
28
30
|
throw new Error("session has no resumable turn");
|
|
29
31
|
return Object.freeze({
|
|
30
32
|
conversation,
|
|
31
|
-
persistence: new SessionPersistence(store, id, lease, snapshot),
|
|
33
|
+
persistence: new SessionPersistence(store, id, lease, snapshot, id),
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
catch (error) {
|
|
35
|
-
await lease.close();
|
|
37
|
+
await lease.close().catch(() => undefined);
|
|
36
38
|
throw error;
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -45,12 +47,16 @@ export class SessionPersistence {
|
|
|
45
47
|
get sessionId() {
|
|
46
48
|
return this.#sessionId;
|
|
47
49
|
}
|
|
50
|
+
/** Reserved before publication so provider cache identity survives resume. */
|
|
51
|
+
get conversationId() {
|
|
52
|
+
return this.#conversationId;
|
|
53
|
+
}
|
|
48
54
|
async checkpoint(conversation) {
|
|
49
55
|
if (this.#failure !== undefined)
|
|
50
56
|
throw this.#failure;
|
|
51
57
|
try {
|
|
52
58
|
if (this.#sessionId === null) {
|
|
53
|
-
const published = await this.#store.publish(conversation, true);
|
|
59
|
+
const published = await this.#store.publish(conversation, true, this.#conversationId);
|
|
54
60
|
this.#lease = published.lease;
|
|
55
61
|
this.#sessionId = published.meta.id;
|
|
56
62
|
this.#snapshot = published;
|
|
@@ -60,7 +66,7 @@ export class SessionPersistence {
|
|
|
60
66
|
throw new Error("session persistence has no verified owner snapshot");
|
|
61
67
|
}
|
|
62
68
|
await this.#lease.assertOwned();
|
|
63
|
-
this.#snapshot = await this.#store.checkpoint(this.#snapshot, conversation);
|
|
69
|
+
this.#snapshot = await this.#store.checkpoint(this.#snapshot, conversation, this.#lease);
|
|
64
70
|
}
|
|
65
71
|
catch (error) {
|
|
66
72
|
this.#failure = error;
|
|
@@ -71,6 +77,7 @@ export class SessionPersistence {
|
|
|
71
77
|
await this.#lease?.close();
|
|
72
78
|
this.#lease = undefined;
|
|
73
79
|
this.#sessionId = null;
|
|
80
|
+
this.#conversationId = reserveSessionId();
|
|
74
81
|
this.#snapshot = undefined;
|
|
75
82
|
this.#failure = undefined;
|
|
76
83
|
}
|