@jskit-ai/assistant-core 0.2.8 → 0.2.10

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.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.8",
11
+ "@jskit-ai/assistant-core": "0.2.10",
12
12
  "@jskit-ai/http-runtime": "0.2.4",
13
13
  "@jskit-ai/kernel": "0.2.4",
14
14
  "@mdi/js": "^7.4.47",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.2.4",
18
- "@jskit-ai/connectors-catalog": "0.2.4"
17
+ "@jskit-ai/connectors-core": "0.2.5",
18
+ "@jskit-ai/connectors-catalog": "0.2.5"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
package/locales/en.json CHANGED
@@ -333,6 +333,7 @@
333
333
  "compactWhileWorking": "Claude is still working. Wait for it to finish before freeing its context.",
334
334
  "noActiveTurn": "Claude has no active turn for this message.",
335
335
  "steeringUnavailable": "This Claude turn cannot accept steering now.",
336
+ "settingsChangeWhileWorking": "Stop the current Claude turn before changing its settings.",
336
337
  "notConnected": "Claude Code is not connected.",
337
338
  "accountStatusInvalid": "Claude Code returned an invalid account status."
338
339
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -56,9 +56,11 @@ export function createClaudePromptDelivery({
56
56
  }
57
57
  let native;
58
58
  try {
59
- if (!command && entry.main && entry.turn?.active && entry.process) {
60
- // Main steering keeps the active instruction installation, just like
61
- // command steering. Refresh guidance on the next idle admission.
59
+ if (!command && entry.turn?.active && entry.process) {
60
+ // Steering keeps the active instruction installation, just like command
61
+ // steering, in Main and in every other conversation: guidance changed
62
+ // by the running work (a repair editing the project's Genesis files)
63
+ // applies at the next idle admission and never refuses the message.
62
64
  // Account replacement must still confirm cleanup before any new send.
63
65
  if (preparation.account) await bindAccount(entry);
64
66
  native = entry.process || await ensureProcess(entry, input);
@@ -163,7 +163,14 @@ export function codexAppServerProcessedResultEvent(session = {}, threadId = "",
163
163
  )) || null;
164
164
  }
165
165
 
166
- export function codexAppServerTurnStateFromAgentRun(run = {}, normalizeRunState = normalizeCodexAppServerRunState) {
166
+ /**
167
+ * The turn a saved Codex run reports. No saved run (null or undefined) is no turn: it is idle. A saved run
168
+ * whose state is not written yet is starting, which counts as active.
169
+ * @param {object | null | undefined} run
170
+ * @param {(state: unknown) => string} [normalizeRunState]
171
+ */
172
+ export function codexAppServerTurnStateFromAgentRun(run, normalizeRunState = normalizeCodexAppServerRunState) {
173
+ if (run == null) return codexAppServerIdleTurnState();
167
174
  const runState = normalizeRunState(run.state);
168
175
  const active = codexAppServerRunStateIsActive(runState);
169
176
  const state = runState === CODEX_APP_SERVER_RUN_STATE.FINALIZING
@@ -194,10 +201,11 @@ export function codexAppServerTurnStateFromAgentRun(run = {}, normalizeRunState
194
201
  }
195
202
 
196
203
  export function codexAppServerTurnState(session = {}, normalizeRunState = normalizeCodexAppServerRunState) {
197
- const run = codexAppServerAgentRun(session);
198
- if (run) {
199
- return codexAppServerTurnStateFromAgentRun(run, normalizeRunState);
200
- }
204
+ return codexAppServerTurnStateFromAgentRun(codexAppServerAgentRun(session), normalizeRunState);
205
+ }
206
+
207
+ /** The turn of a session or binding that has no saved Codex run. */
208
+ function codexAppServerIdleTurnState() {
201
209
  return {
202
210
  active: false,
203
211
  completedAt: "",
@@ -1,3 +1,4 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
1
2
  import { createConversationSystemPrompt } from "../systemPrompt.js";
2
3
 
3
4
  /**
@@ -44,7 +45,12 @@ export function createClaudeConversationAdapter({ getProcess, isActive, startPro
44
45
  const canUpdate = getProcess() && installedPrompt === systemPrompt && installedMode === instructionMode &&
45
46
  liveUpdateIdentity !== undefined && updateIdentity === liveUpdateIdentity;
46
47
  if (getProcess() && isActive() && !canUpdate && !instructions.isCurrent({ systemPrompt, contextIdentity })) {
47
- throw new Error("Stop the current Claude turn before changing its settings.");
48
+ // Steering never gets here: it keeps the running installation. This is an
49
+ // explicit settings or command preparation while work is running.
50
+ throw new AppError(409, "Stop the current Claude turn before changing its settings.", {
51
+ code: "conversation_claude_settings_change_while_working",
52
+ messageCode: "assistant.claude.settingsChangeWhileWorking"
53
+ });
48
54
  }
49
55
  return instructions.ensure({
50
56
  systemPrompt,
@@ -949,6 +949,36 @@ test("Claude status retains active instructions and applies changed guidance onc
949
949
  assert.equal(f.processes.length, 2, "Idle readiness installs the changed instructions");
950
950
  });
951
951
 
952
+ // A repair chat edits the project's Genesis files while it works, so its
953
+ // composed instructions change mid-turn. A message typed then is steering.
954
+ for (const main of [true, false]) test(`Claude steering keeps the running instructions after guidance changed mid-turn and idle admission applies them, main=${main}`, async t => {
955
+ const f = await publishedClaudeFixture(t);
956
+ const entry = main ? f.entry : await f.owner.open(f.context, {
957
+ conversationId: "87654321-4321-4321-8321-cba987654321", mainId: f.conversationId, create: true
958
+ });
959
+ assert.equal(entry.main, main);
960
+ await f.owner.startTurn(entry, { message: "Repair the setup", messageId: "repair", persistent: true });
961
+ const running = entry.process;
962
+ f.behavior.systemPrompt = "Guidance the repair changed during its own turn.";
963
+ const steered = await f.owner.startTurn(entry, { message: "The saved version already has this", messageId: "midturn", steer: true });
964
+ assert.deepEqual([steered.ok, entry.nativeTurn.read().active], [true, true]);
965
+ assert.equal(entry.process, running, "the steer keeps the running process and its instructions");
966
+ assert.equal(f.processes.length, 1);
967
+ assert.equal(f.behavior.nativeInputs.at(-1), "The saved version already has this", "the steer reached the running Claude");
968
+ await entry.process.options.onEvent({ session_id: entry.id, type: "result", subtype: "success", result: "Done" });
969
+ await f.owner.startTurn(entry, { message: "Next request", messageId: "next" });
970
+ assert.equal(f.processes.length, 2, "the next idle admission installs the changed guidance");
971
+ });
972
+
973
+ test("an explicit Claude settings change while work runs is refused with its own code", async t => {
974
+ const f = await publishedClaudeFixture(t);
975
+ await f.send();
976
+ await assert.rejects(f.entry.nativeTurn.prepare({ systemPrompt: "Changed guidance", settings: {}, model: "sonnet" }), error =>
977
+ error.code === "conversation_claude_settings_change_while_working" && error.statusCode === 409 &&
978
+ error.messageCode === "assistant.claude.settingsChangeWhileWorking");
979
+ assert.equal(f.processes.length, 1);
980
+ });
981
+
952
982
  test("Claude active status keeps a running turn across an account change; idle admission applies it", async t => {
953
983
  const f = await publishedClaudeFixture(t);
954
984
  await f.send();
@@ -1740,6 +1740,28 @@ test("Codex cancellation drains owned work, and a deadline saves a failed turn",
1740
1740
  assert.equal(trace.filter(row => row.args).length, 1);
1741
1741
  });
1742
1742
 
1743
+ // An upgraded binding keeps its native thread but has no saved Codex run. With
1744
+ // its Codex account gone, reading, Stop and selection still start nothing.
1745
+ test("a Codex binding with no saved run reads ready, and Stop and selection start no Codex work", async t => {
1746
+ const f = await fixture(t, apiOptions());
1747
+ await f.first.close();
1748
+ await f.storage.write("conversation", async transaction => {
1749
+ const metadata = await transaction.readMetadata();
1750
+ assert.equal(Object.hasOwn(metadata.runtime.binding, "codexAppServerRun"), false);
1751
+ metadata.runtime.binding = { ...metadata.runtime.binding, threadId: "imported-thread", processInstanceId: "" };
1752
+ await transaction.writeMetadata(metadata);
1753
+ });
1754
+ await rm(f.account);
1755
+ const conversation = await f.runtime().open({ id: "conversation" });
1756
+ const state = await conversation.read();
1757
+ assert.equal(state.status, "ready");
1758
+ assert.equal(state.phase, "");
1759
+ assert.deepEqual(await conversation.cancel(), { stopped: false });
1760
+ await select(conversation, "api", "imported-to-api");
1761
+ assert.equal((await conversation.read()).engine, "api");
1762
+ await assert.rejects(f.trace(), { code: "ENOENT" }, "No Codex process was started");
1763
+ });
1764
+
1743
1765
  test("lost Codex acknowledgement stays uncertain until native history proves the exact message", async t => {
1744
1766
  const f = await fixture(t);
1745
1767
  const lost = { messageId: "lost-id", text: "lost" };
@@ -19,6 +19,7 @@ import {
19
19
  codexAppServerAgentRunPatch,
20
20
  codexAppServerPendingUserMessageOwnership,
21
21
  codexAppServerTurnState,
22
+ codexAppServerTurnStateFromAgentRun,
22
23
  CODEX_APP_SERVER_RUN_STATE
23
24
  } from "../src/server/conversation/codexTurn.js";
24
25
 
@@ -563,6 +564,18 @@ test("original pending ownership and terminal outer identity are preserved witho
563
564
  error => error === hostError);
564
565
  });
565
566
 
567
+ test("no saved Codex run is no turn, while a saved run without a state is starting", () => {
568
+ for (const run of [undefined, null]) {
569
+ const turn = codexAppServerTurnStateFromAgentRun(run);
570
+ assert.equal(turn.active, false);
571
+ assert.equal(turn.state, "idle");
572
+ assert.deepEqual(turn, codexAppServerTurnState({ agentRuns: [] }));
573
+ }
574
+ const starting = codexAppServerTurnStateFromAgentRun({});
575
+ assert.equal(starting.active, true);
576
+ assert.equal(starting.state, "starting");
577
+ });
578
+
566
579
  test("native notification queues merge only waiting stream deltas and retain receipt order", async () => {
567
580
  const owner = createCodexAppServerNotificationQueue();
568
581
  const context = { sessionId: "session-1", sessionKey: "session-1", threadId: "thread-1", turnId: "turn-1" };