@giovannijecha/jecode 0.8.4 → 0.8.6

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.
Files changed (99) hide show
  1. package/README.md +28 -9
  2. package/dist/accounts.js +47 -10
  3. package/dist/atomic.js +24 -14
  4. package/dist/batch-view.js +27 -2
  5. package/dist/batch.js +39 -4
  6. package/dist/bounded-file.js +212 -0
  7. package/dist/cli-info.js +0 -1
  8. package/dist/commands.js +2 -0
  9. package/dist/config.js +16 -7
  10. package/dist/context/automatic.js +35 -0
  11. package/dist/context/compactor.js +32 -2
  12. package/dist/context/manual.js +20 -3
  13. package/dist/context/request-projection.js +130 -0
  14. package/dist/controller-request.js +33 -11
  15. package/dist/credential-commands.js +25 -9
  16. package/dist/credentials.js +56 -18
  17. package/dist/directory-anchor.js +91 -0
  18. package/dist/file-identity.js +12 -0
  19. package/dist/model-command.js +5 -4
  20. package/dist/openai-account-command.js +23 -10
  21. package/dist/openai-account.js +7 -5
  22. package/dist/openai-oauth-callback.js +13 -4
  23. package/dist/openai-oauth-tokens.js +9 -7
  24. package/dist/openai-oauth.js +8 -6
  25. package/dist/permission-command.js +1 -1
  26. package/dist/process-lease.js +329 -0
  27. package/dist/provider-commands.js +11 -37
  28. package/dist/provider-errors.js +31 -7
  29. package/dist/provider-label.js +9 -3
  30. package/dist/providers/anthropic-stream.js +4 -1
  31. package/dist/providers/anthropic-wire.js +8 -3
  32. package/dist/providers/anthropic.js +39 -20
  33. package/dist/providers/catalog.js +4 -4
  34. package/dist/providers/failure.js +181 -0
  35. package/dist/providers/http.js +82 -23
  36. package/dist/providers/index.js +1 -5
  37. package/dist/providers/ollama-context.js +42 -0
  38. package/dist/providers/ollama-endpoint.js +7 -34
  39. package/dist/providers/ollama-stream.js +5 -1
  40. package/dist/providers/ollama.js +36 -158
  41. package/dist/providers/openai-codex.js +71 -45
  42. package/dist/providers/openai-stream.js +26 -2
  43. package/dist/providers/openai.js +51 -25
  44. package/dist/providers/sse.js +52 -8
  45. package/dist/request-identity.js +32 -0
  46. package/dist/sessions/bucket.js +55 -0
  47. package/dist/sessions/catalog-io.js +162 -0
  48. package/dist/sessions/catalog.js +3 -1
  49. package/dist/sessions/codec-messages.js +122 -0
  50. package/dist/sessions/codec-transcript.js +93 -0
  51. package/dist/sessions/codec-values.js +52 -0
  52. package/dist/sessions/codec.js +4 -257
  53. package/dist/sessions/files.js +158 -0
  54. package/dist/sessions/lease.js +132 -49
  55. package/dist/sessions/load.js +90 -0
  56. package/dist/sessions/runtime.js +15 -8
  57. package/dist/sessions/snapshot.js +33 -0
  58. package/dist/sessions/store.js +183 -378
  59. package/dist/settings-command.js +10 -5
  60. package/dist/settings.js +75 -22
  61. package/dist/stable-directory.js +148 -0
  62. package/dist/start.js +1 -2
  63. package/dist/store-lock.js +68 -84
  64. package/dist/tools/args.js +2 -2
  65. package/dist/tools/file-read.js +192 -0
  66. package/dist/tools/file-summary.js +9 -0
  67. package/dist/tools/{fs.js → file-write.js} +5 -171
  68. package/dist/tools/glob.js +107 -0
  69. package/dist/tools/index.js +2 -1
  70. package/dist/tools/search.js +66 -205
  71. package/dist/tools/text-boundary.js +7 -33
  72. package/dist/tui/app-workflows.js +9 -334
  73. package/dist/tui/approve.js +5 -3
  74. package/dist/tui/blocks.js +8 -7
  75. package/dist/tui/command-workflow.js +106 -0
  76. package/dist/tui/components/command-menu.js +7 -10
  77. package/dist/tui/components/footer.js +1 -1
  78. package/dist/tui/components/menu.js +74 -43
  79. package/dist/tui/components/messages.js +16 -9
  80. package/dist/tui/components/tool-evidence.js +107 -0
  81. package/dist/tui/components/tool-motion.js +32 -0
  82. package/dist/tui/components/tool.js +48 -202
  83. package/dist/tui/feedback.js +4 -0
  84. package/dist/tui/help.js +1 -1
  85. package/dist/tui/picker-layout.js +40 -0
  86. package/dist/tui/picker.js +7 -71
  87. package/dist/tui/session-view.js +7 -2
  88. package/dist/tui/tool-details.js +135 -0
  89. package/dist/tui/transcript-grammar.js +8 -1
  90. package/dist/tui/transcript-view.js +26 -112
  91. package/dist/tui/turn-workflow.js +264 -0
  92. package/dist/tui/turn.js +7 -140
  93. package/dist/tui/workflow-types.js +2 -0
  94. package/dist/tui/workspace.js +21 -7
  95. package/dist/user-store.js +23 -31
  96. package/package.json +14 -15
  97. package/dist/ollama-settings-command.js +0 -74
  98. package/dist/tools/ripgrep.js +0 -230
  99. package/dist/tui/motion.js +0 -32
@@ -0,0 +1,90 @@
1
+ // Strict full-session loading and one adjacent checkpoint recovery.
2
+ import * as path from "node:path";
3
+ import { atomicWrite } from "../atomic.js";
4
+ import { ConversationTree } from "../conversation.js";
5
+ import { assertDirectoryAnchor, captureDirectDirectory } from "../directory-anchor.js";
6
+ import { assertSessionId } from "./bucket.js";
7
+ import { sameSessionHead, sessionCatalog } from "./catalog.js";
8
+ import { decodeHead, decodeMeta, encodeHead, SESSION_FILE_LIMITS, SESSION_SCHEMA } from "./codec.js";
9
+ import { FILE_MODE, readJson, readNodes } from "./files.js";
10
+ import { sessionLeaseOwns } from "./lease.js";
11
+ export async function loadSession(bucket, id, scope, recoveryLease) {
12
+ assertSessionId(id);
13
+ await bucket.assert();
14
+ const directory = bucket.directory(id);
15
+ const directoryAnchor = await captureDirectDirectory(directory, "session directory");
16
+ const nodesAnchor = await captureDirectDirectory(path.join(directory, "nodes"), "session node directory");
17
+ const validateSession = async () => {
18
+ await Promise.all([
19
+ bucket.assert(),
20
+ assertDirectoryAnchor(directoryAnchor),
21
+ assertDirectoryAnchor(nodesAnchor),
22
+ ]);
23
+ };
24
+ const meta = decodeMeta(await readJson(path.join(directory, "meta.json"), SESSION_FILE_LIMITS.metadataBytes, undefined, validateSession));
25
+ bucket.assertWorkspace(meta, id);
26
+ const persistedHead = decodeHead(await readJson(path.join(directory, "head.json"), SESSION_FILE_LIMITS.metadataBytes, undefined, validateSession));
27
+ let head = persistedHead;
28
+ const stored = await readNodes(nodesAnchor, validateSession);
29
+ const ahead = stored.filter((entry) => entry.sequence > head.sequence);
30
+ if (ahead.some((entry) => entry.sequence !== head.sequence + 1) || ahead.length > 1) {
31
+ throw new Error("session has an ambiguous incomplete checkpoint");
32
+ }
33
+ const byId = new Map(stored.map((entry) => [entry.node.id, entry]));
34
+ const headed = byId.get(head.nodeId);
35
+ if (headed === undefined)
36
+ throw new Error("session head is missing its conversation node");
37
+ if (ahead.length === 0) {
38
+ if (headed.sequence !== head.sequence || headed.node.revision !== head.revision ||
39
+ headed.node.parentId !== head.parentId) {
40
+ throw new Error("session head does not match its conversation node");
41
+ }
42
+ }
43
+ else {
44
+ const candidate = ahead[0];
45
+ const replacesHead = candidate.node.id === head.nodeId &&
46
+ candidate.node.parentId === head.parentId &&
47
+ candidate.node.revision === head.revision + 1;
48
+ const candidateParent = byId.get(candidate.node.parentId);
49
+ const extendsTree = candidate.node.id === stored.length &&
50
+ candidate.node.revision === 1 && candidateParent !== undefined &&
51
+ candidateParent.sequence <= head.sequence;
52
+ if (!replacesHead && !extendsTree) {
53
+ throw new Error("session checkpoint cannot be recovered safely");
54
+ }
55
+ if (recoveryLease === undefined ||
56
+ !sessionLeaseOwns(recoveryLease, id, scope)) {
57
+ throw new Error("session recovery requires exclusive ownership");
58
+ }
59
+ await recoveryLease.assertOwned();
60
+ head = Object.freeze({
61
+ version: SESSION_SCHEMA,
62
+ sequence: candidate.sequence,
63
+ nodeId: candidate.node.id,
64
+ parentId: candidate.node.parentId,
65
+ revision: candidate.node.revision,
66
+ updatedAt: candidate.updatedAt,
67
+ });
68
+ }
69
+ // Recovery must pass all turn, tree, and catalogue invariants before it
70
+ // changes durable state; the file codec alone cannot validate the tree.
71
+ const nodes = stored.map((entry) => entry.node);
72
+ const conversation = ConversationTree.restore(nodes, head.nodeId);
73
+ const catalog = sessionCatalog(meta, head, conversation);
74
+ if (head !== persistedHead && recoveryLease !== undefined) {
75
+ await atomicWrite(path.join(directory, "head.json"), encodeHead(head), {
76
+ mode: FILE_MODE,
77
+ validate: async (phase) => {
78
+ await validateSession();
79
+ if (phase !== "before-rename")
80
+ return;
81
+ await recoveryLease.assertOwned();
82
+ const current = decodeHead(await readJson(path.join(directory, "head.json"), SESSION_FILE_LIMITS.metadataBytes, undefined, validateSession));
83
+ if (!sameSessionHead(current, persistedHead)) {
84
+ throw new Error("session head changed while recovering its checkpoint");
85
+ }
86
+ },
87
+ });
88
+ }
89
+ return Object.freeze({ meta, head, conversation, catalog });
90
+ }
@@ -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
  }
@@ -0,0 +1,33 @@
1
+ // Verified in-memory checkpoint identity shared by persistence operations.
2
+ import { catalogMatches, encodeSessionCatalog } from "./catalog.js";
3
+ import { encodeHead, encodeMeta } from "./codec.js";
4
+ import { assertSessionId, workspaceKey } from "./bucket.js";
5
+ export function assertSharedNodes(previous, next, replacedId) {
6
+ for (const node of previous.nodes) {
7
+ if (node.id === replacedId)
8
+ continue;
9
+ const candidate = next.node(node.id);
10
+ if (candidate !== node) {
11
+ throw new Error("session checkpoint rewrites prior conversation history");
12
+ }
13
+ }
14
+ }
15
+ export function assertSnapshot(snapshot, workspaceRoot, workspaceDigest) {
16
+ encodeMeta(snapshot.meta);
17
+ encodeHead(snapshot.head);
18
+ encodeSessionCatalog(snapshot.catalog);
19
+ assertSessionId(snapshot.meta.id);
20
+ if (snapshot.meta.workspaceDigest !== workspaceDigest ||
21
+ workspaceKey(snapshot.meta.workspaceRoot) !== workspaceKey(workspaceRoot))
22
+ throw new Error("session snapshot belongs to a different workspace");
23
+ const active = snapshot.conversation.activeNode;
24
+ if (active === undefined ||
25
+ active.id !== snapshot.head.nodeId ||
26
+ active.parentId !== snapshot.head.parentId ||
27
+ active.revision !== snapshot.head.revision ||
28
+ snapshot.head.sequence < snapshot.conversation.nodes.length)
29
+ throw new Error("session snapshot does not match its verified head");
30
+ if (!catalogMatches(snapshot.catalog, snapshot.meta, snapshot.head)) {
31
+ throw new Error("session snapshot does not match its verified catalogue");
32
+ }
33
+ }