@vincemakes/kiso-code 0.15.1 → 0.15.2

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/dist/dispatch.js CHANGED
@@ -4,9 +4,10 @@
4
4
  * the context (the chain, the run state, the prompt arming).
5
5
  */
6
6
  import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modelPickView, palette } from "@vincemakes/kiso-tui";
7
+ import { newSessionId } from "./session-id.js";
7
8
  import { buildAdapter } from "@vincemakes/kiso-runtime/internal";
8
9
  import { MODES, getMode, setMode } from "./mode.js";
9
- import { agentModel, body, bodyLog, configModels, dock, readContextLedger, setAgentModel, setCurrentModelName } from "./state.js";
10
+ import { agentModel, body, bodyLog, configModels, dock, readContextLedger, sessionsDir, setAgentModel, setCurrentModelName } from "./state.js";
10
11
  import { directWriteProfile, profileAvailable } from "./config.js";
11
12
  /** The ONE dispatcher — slash commands, exit, and turns. The recovery
12
13
  * replay routes through it too — a queued "/last" must never become a
@@ -383,7 +384,7 @@ export function dispatch(line, ctx) {
383
384
  ctx.input.prompt();
384
385
  return;
385
386
  }
386
- ctx.requestSwitch(new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16));
387
+ ctx.requestSwitch(newSessionId(sessionsDir()));
387
388
  return;
388
389
  }
389
390
  if (trimmed === "/resume" || trimmed.startsWith("/resume ")) {
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * makeAgent, and main.
25
25
  */
26
26
  import { appendFileSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
27
+ import { newSessionId } from "./session-id.js";
27
28
  import { createInterface } from "node:readline";
28
29
  import { fileURLToPath } from "node:url";
29
30
  import { join } from "node:path";
@@ -755,7 +756,7 @@ async function main() {
755
756
  if (printPrompt !== undefined) {
756
757
  // the -p flow: recovery-first one-shot, the resume() machinery
757
758
  // verbatim (a fresh id makes the recovery a no-op)
758
- const id = command ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
759
+ const id = command ?? newSessionId(sessionsDir());
759
760
  agent = await makeAgent(id, input, modelFlag);
760
761
  applyConfigMode();
761
762
  const session = await agent.session({ id });
@@ -767,7 +768,7 @@ async function main() {
767
768
  }
768
769
  switch (command) {
769
770
  case "chat": {
770
- const id = arg ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
771
+ const id = arg ?? newSessionId(sessionsDir());
771
772
  // v2b: the dock (TTY only) wraps the whole session — the
772
773
  // trust question, the banner, the body, and the input line.
773
774
  dock.enter();
@@ -884,7 +885,7 @@ async function main() {
884
885
  default: {
885
886
  // A area: no subcommand (or any non-command first argument) IS
886
887
  // chat — the first argument is the session id.
887
- const id = command ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
888
+ const id = command ?? newSessionId(sessionsDir());
888
889
  dock.enter();
889
890
  // R-I-p2 (finding R-I-p-2): the bare command passes the SAME
890
891
  // input source and model flag as chat/resume — the pre-patch
@@ -0,0 +1,54 @@
1
+ /**
2
+ * RD1B-F9 — the auto-generated session id, in ONE place.
3
+ *
4
+ * It used to be this expression, copied at four call sites:
5
+ *
6
+ * new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16)
7
+ *
8
+ * which stops at the MINUTE and carries no entropy. `SessionStore` is one
9
+ * file per id, so two sessions started in the same minute were the same
10
+ * session: the second launch presented as fresh and transparently appended
11
+ * to the first one's durable log (`tests/session-id-identity.test.ts`).
12
+ *
13
+ * WHAT THE GUARANTEE IS, precisely — because the first version of this
14
+ * comment claimed "collision-safe at any launch rate a human or a script
15
+ * produces" and that was an unmeasured claim that is false. A 16-bit
16
+ * suffix collides at script rates: 100 launches inside one second carry a
17
+ * 7.3% chance of at least one collision, and 1,000 produce a handful every
18
+ * time. Measured, not modelled.
19
+ *
20
+ * So the id does not rest on entropy at all:
21
+ *
22
+ * - SEQUENTIAL collision is eliminated BY CONSTRUCTION. `newSessionId`
23
+ * is handed the sessions directory and will not return an id whose
24
+ * durable log or lock already exists; it draws again. Entropy only
25
+ * decides how often it has to draw.
26
+ * - CONCURRENT collision — two processes drawing the same id before
27
+ * either has written — remains possible and is already handled
28
+ * correctly one layer down: the store's single-writer link lock
29
+ * (ADR-0050) fails the second writer loudly, and `storage.test.ts`
30
+ * pins that. Loud failure is the right outcome there; silent sharing
31
+ * was the defect.
32
+ *
33
+ * The id keeps the one property anything depends on: **lexicographic order
34
+ * is time order**, because `listSessions` sorts with `id.localeCompare`
35
+ * and nothing anywhere parses an id back into a date. Seconds extend the
36
+ * stamp monotonically; the suffix only breaks ties inside one second.
37
+ *
38
+ * It also stays 24 characters — exactly the session picker's id column cap
39
+ * (`packages/tui/src/session-picker.ts:112`). Widening the suffix instead
40
+ * of checking for collisions would have pushed the distinguishing tail out
41
+ * of the column, hiding the very bytes that make two ids different.
42
+ *
43
+ * Old ids are untouched — no rename, no migration. They still resume by id
44
+ * and still sort before same-minute new ids.
45
+ */
46
+ /**
47
+ * A fresh session id: `YYYY-MM-DDTHH-MM-SS-xxxx`, sortable, and — when
48
+ * `dir` is given — guaranteed not to name a session that already exists
49
+ * there.
50
+ *
51
+ * `rand` is injectable so the collision path can be tested; production
52
+ * never passes it.
53
+ */
54
+ export declare function newSessionId(dir?: string, now?: Date, rand?: () => string): string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * RD1B-F9 — the auto-generated session id, in ONE place.
3
+ *
4
+ * It used to be this expression, copied at four call sites:
5
+ *
6
+ * new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16)
7
+ *
8
+ * which stops at the MINUTE and carries no entropy. `SessionStore` is one
9
+ * file per id, so two sessions started in the same minute were the same
10
+ * session: the second launch presented as fresh and transparently appended
11
+ * to the first one's durable log (`tests/session-id-identity.test.ts`).
12
+ *
13
+ * WHAT THE GUARANTEE IS, precisely — because the first version of this
14
+ * comment claimed "collision-safe at any launch rate a human or a script
15
+ * produces" and that was an unmeasured claim that is false. A 16-bit
16
+ * suffix collides at script rates: 100 launches inside one second carry a
17
+ * 7.3% chance of at least one collision, and 1,000 produce a handful every
18
+ * time. Measured, not modelled.
19
+ *
20
+ * So the id does not rest on entropy at all:
21
+ *
22
+ * - SEQUENTIAL collision is eliminated BY CONSTRUCTION. `newSessionId`
23
+ * is handed the sessions directory and will not return an id whose
24
+ * durable log or lock already exists; it draws again. Entropy only
25
+ * decides how often it has to draw.
26
+ * - CONCURRENT collision — two processes drawing the same id before
27
+ * either has written — remains possible and is already handled
28
+ * correctly one layer down: the store's single-writer link lock
29
+ * (ADR-0050) fails the second writer loudly, and `storage.test.ts`
30
+ * pins that. Loud failure is the right outcome there; silent sharing
31
+ * was the defect.
32
+ *
33
+ * The id keeps the one property anything depends on: **lexicographic order
34
+ * is time order**, because `listSessions` sorts with `id.localeCompare`
35
+ * and nothing anywhere parses an id back into a date. Seconds extend the
36
+ * stamp monotonically; the suffix only breaks ties inside one second.
37
+ *
38
+ * It also stays 24 characters — exactly the session picker's id column cap
39
+ * (`packages/tui/src/session-picker.ts:112`). Widening the suffix instead
40
+ * of checking for collisions would have pushed the distinguishing tail out
41
+ * of the column, hiding the very bytes that make two ids different.
42
+ *
43
+ * Old ids are untouched — no rename, no migration. They still resume by id
44
+ * and still sort before same-minute new ids.
45
+ */
46
+ import { randomBytes } from "node:crypto";
47
+ import { existsSync } from "node:fs";
48
+ import { join } from "node:path";
49
+ /** How many draws before giving up. Reaching this means either the clock
50
+ * is frozen or the directory holds ~every suffix for this second; both
51
+ * are worth failing loudly over rather than returning a colliding id. */
52
+ const MAX_DRAWS = 50;
53
+ const stampOf = (now, suffix) => `${now.toISOString().replace(/[:.]/g, "-").slice(0, 19)}-${suffix}`;
54
+ /**
55
+ * A fresh session id: `YYYY-MM-DDTHH-MM-SS-xxxx`, sortable, and — when
56
+ * `dir` is given — guaranteed not to name a session that already exists
57
+ * there.
58
+ *
59
+ * `rand` is injectable so the collision path can be tested; production
60
+ * never passes it.
61
+ */
62
+ export function newSessionId(dir, now = new Date(), rand = () => randomBytes(2).toString("hex")) {
63
+ if (dir === undefined)
64
+ return stampOf(now, rand());
65
+ for (let draw = 0; draw < MAX_DRAWS; draw += 1) {
66
+ const id = stampOf(now, rand());
67
+ // The store writes `<id>.jsonl` and takes `<id>.lock`; either one
68
+ // present means the id is spoken for, including by a session that
69
+ // has locked but not yet appended.
70
+ if (!existsSync(join(dir, `${id}.jsonl`)) && !existsSync(join(dir, `${id}.lock`)))
71
+ return id;
72
+ }
73
+ throw new Error(`could not draw an unused session id in ${dir} after ${MAX_DRAWS} attempts`);
74
+ }
package/dist/trust-ui.js CHANGED
@@ -70,6 +70,28 @@ opts) {
70
70
  resolve(verdict);
71
71
  }, opts);
72
72
  }
73
+ else if (view.ask) {
74
+ // RD1B-F6: a multiple-choice ask has NO dock-less form. The
75
+ // fallback said so — "the question is declined" — and then
76
+ // waited for a line anyway, forever, on input nothing in the
77
+ // environment knows to send: the surface has just announced
78
+ // the interaction is over. An unattended run did not fail
79
+ // there, it stopped, silently (RD-1B c9-r2).
80
+ //
81
+ // So the sentence becomes true. There is no panel, therefore
82
+ // no answer is obtainable, therefore the ask declines NOW and
83
+ // the model gets an honest refusal to act on. Waiting could
84
+ // only ever have produced the same decline, later, and every
85
+ // line typed at it was discarded anyway.
86
+ //
87
+ // The y/n fallback below still serves the views that really
88
+ // do take a yes or no — the uncertainty gate, the trust
89
+ // prompt, the verify offer. Those have a dock-less form.
90
+ settled = true;
91
+ pendingAsk = null;
92
+ bodyLog(view.fallbackQuestion);
93
+ resolve({ action: "deny", reason: "no option panel in this terminal" });
94
+ }
73
95
  else {
74
96
  // v2c: a TTY without a dock (rows < 4) — the fallback question
75
97
  // in the body; the y/n line answer maps to the verdicts.
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
1
  {
2
- "name": "@vincemakes/kiso-code",
3
- "version": "0.15.1",
4
- "description": "kiso CLI the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
5
- "type": "module",
6
- "license": "MIT",
7
- "bin": {
8
- "kiso": "dist/index.js"
9
- },
10
- "files": [
11
- "dist",
12
- "README.md",
13
- "LICENSE"
14
- ],
15
- "scripts": {
16
- "build": "tsc -p tsconfig.build.json",
17
- "typecheck": "tsc -p tsconfig.json",
18
- "test": "vitest run"
19
- },
20
- "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.15.1",
22
- "@vincemakes/kiso-core": "0.15.1",
23
- "@vincemakes/kiso-evals": "0.15.1",
24
- "@vincemakes/kiso-mcp-ext": "0.15.1",
25
- "@vincemakes/kiso-provider-anthropic": "0.15.1",
26
- "@vincemakes/kiso-provider-openai": "0.15.1",
27
- "@vincemakes/kiso-runtime": "0.15.1",
28
- "@vincemakes/kiso-skills-ext": "0.15.1",
29
- "@vincemakes/kiso-subagent-ext": "0.15.1",
30
- "@vincemakes/kiso-task-ext": "0.15.1",
31
- "@vincemakes/kiso-tools-node": "0.15.1",
32
- "@vincemakes/kiso-tui": "0.15.1",
33
- "@vincemakes/kiso-tui-cells": "0.15.1"
34
- },
35
- "devDependencies": {
36
- "@types/node": "^26.1.2",
37
- "typescript": "^5.7.2",
38
- "vitest": "^3.0.0"
39
- },
40
- "engines": {
41
- "node": ">=22"
42
- },
43
- "os": [
44
- "darwin",
45
- "linux"
46
- ],
47
- "repository": {
48
- "type": "git",
49
- "url": "https://github.com/vincemakes/kiso.git",
50
- "directory": "apps/cli"
51
- },
52
- "bugs": {
53
- "url": "https://github.com/vincemakes/kiso/issues"
54
- },
55
- "homepage": "https://github.com/vincemakes/kiso/tree/main/apps/cli#readme"
2
+ "name": "@vincemakes/kiso-code",
3
+ "version": "0.15.2",
4
+ "description": "kiso CLI \u2014 the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "kiso": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "typecheck": "tsc -p tsconfig.json",
18
+ "test": "vitest run"
19
+ },
20
+ "dependencies": {
21
+ "@vincemakes/kiso-ask-ext": "0.15.2",
22
+ "@vincemakes/kiso-core": "0.15.2",
23
+ "@vincemakes/kiso-evals": "0.15.2",
24
+ "@vincemakes/kiso-mcp-ext": "0.15.2",
25
+ "@vincemakes/kiso-provider-anthropic": "0.15.2",
26
+ "@vincemakes/kiso-provider-openai": "0.15.2",
27
+ "@vincemakes/kiso-runtime": "0.15.2",
28
+ "@vincemakes/kiso-skills-ext": "0.15.2",
29
+ "@vincemakes/kiso-subagent-ext": "0.15.2",
30
+ "@vincemakes/kiso-task-ext": "0.15.2",
31
+ "@vincemakes/kiso-tools-node": "0.15.2",
32
+ "@vincemakes/kiso-tui": "0.15.2",
33
+ "@vincemakes/kiso-tui-cells": "0.15.2"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^26.1.2",
37
+ "typescript": "^5.7.2",
38
+ "vitest": "^3.0.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=22"
42
+ },
43
+ "os": [
44
+ "darwin",
45
+ "linux"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/vincemakes/kiso.git",
50
+ "directory": "apps/cli"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/vincemakes/kiso/issues"
54
+ },
55
+ "homepage": "https://github.com/vincemakes/kiso/tree/main/apps/cli#readme"
56
56
  }