@skaile/workspaces 1.12.0 → 1.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#422](https://github.com/skaile-ai/workspaces/pull/422) [`47a9896`](https://github.com/skaile-ai/workspaces/commit/47a989633a22c0c090fb7c50c4c3355738f73aca) Thanks [@Frozen666](https://github.com/Frozen666)! - Fix a dropped in-flight prompt when a skill-change driver restart races a live
8
+ turn. The runner now serializes prompt turns against out-of-band driver swaps via
9
+ a bidirectional gate: a swap defers to an in-flight turn (so the driver is never
10
+ killed mid-prompt), and a prompt arriving during a swap binds to the fresh driver.
11
+ Previously the killed SDK process emitted no completion event and the UI hung on
12
+ "AI is thinking…" forever.
13
+
14
+ ## 1.12.1
15
+
16
+ ### Patch Changes
17
+
18
+ - [#419](https://github.com/skaile-ai/workspaces/pull/419) [`da98cc1`](https://github.com/skaile-ai/workspaces/commit/da98cc1b86636267e1f0855134fbeb229869ae2a) Thanks [@henkbla](https://github.com/henkbla)! - serve: stop treating a finished flow run as an active one. `activeFlows` was never pruned, so a run that reached `complete` / `cancelled` permanently blocked `flow.start` in a warm session — with a reject message advertising a remedy ("cancel it first") that could not work, since cancel leaves the entry in place — and kept re-emitting its settled snapshot on every reconnect. The `start` guard now rejects only on a non-terminal run, and a finished run is dropped when the next one registers. `failed` and `paused` stay non-terminal (`retryNode` revives them). A settled run is still replayed on connect until it is superseded: the turn loop keeps driving with no host attached, so that replay is the only path by which a host that was detached when the run finished ever learns it did.
19
+
3
20
  ## 1.12.0
4
21
 
5
22
  ### Minor Changes
@@ -2939,6 +2939,70 @@ function cleanupCloudCredentialFiles(projectDir) {
2939
2939
  }
2940
2940
  }
2941
2941
 
2942
+ // runner/src/driver-swap-gate.ts
2943
+ var DriverSwapGate = class {
2944
+ /** Resolves when the in-flight driver recreate settles (resolve or reject). */
2945
+ activeDriverSwap = null;
2946
+ /** FIFO chain; resolves once every started turn has settled. */
2947
+ turnTail = Promise.resolve();
2948
+ /**
2949
+ * Mark a turn as in flight. Call at the real `driver.prompt()` dispatch — not
2950
+ * earlier — so a same-handler restart (e.g. restaging skills before the turn)
2951
+ * cannot self-deadlock by awaiting the very turn that spawned it. Returns an
2952
+ * idempotent `endTurn`; call it in a `finally` around the dispatch.
2953
+ */
2954
+ beginTurn() {
2955
+ let resolveTurn;
2956
+ const turn = new Promise((resolve4) => {
2957
+ resolveTurn = resolve4;
2958
+ });
2959
+ this.turnTail = this.turnTail.then(() => turn);
2960
+ let ended = false;
2961
+ return () => {
2962
+ if (ended) return;
2963
+ ended = true;
2964
+ resolveTurn();
2965
+ };
2966
+ }
2967
+ /**
2968
+ * Await any in-flight turn before tearing a driver down. Awaits the FIFO tail
2969
+ * (not a single "latest turn" field) so an earlier turn that outlives a later
2970
+ * one still holds the swap off — the tail resolves only once every started
2971
+ * turn has settled. Never throws: a rejected turn is still a settled turn, and
2972
+ * the swap may proceed (the driver is idle either way).
2973
+ */
2974
+ async awaitActiveTurn() {
2975
+ try {
2976
+ await this.turnTail;
2977
+ } catch {
2978
+ }
2979
+ }
2980
+ /**
2981
+ * Register the in-flight recreate promise so concurrent prompts defer to it.
2982
+ * Auto-clears when `recreate` settles (resolve OR reject) so a failed recreate
2983
+ * never blocks prompts forever.
2984
+ */
2985
+ beginSwap(recreate) {
2986
+ this.activeDriverSwap = recreate;
2987
+ const clear = () => {
2988
+ if (this.activeDriverSwap === recreate) this.activeDriverSwap = null;
2989
+ };
2990
+ recreate.then(clear, clear);
2991
+ }
2992
+ /**
2993
+ * Await the in-flight swap before reading the live driver. Re-checks in a loop
2994
+ * so a second swap starting during the wait is also awaited.
2995
+ */
2996
+ async awaitSwap() {
2997
+ while (this.activeDriverSwap) {
2998
+ try {
2999
+ await this.activeDriverSwap;
3000
+ } catch {
3001
+ }
3002
+ }
3003
+ }
3004
+ };
3005
+
2942
3006
  // runner/src/compaction/prompt.ts
2943
3007
  var DEFAULT_COMPACTION_PROMPT = `You are being asked to compact this conversation into a structured summary.
2944
3008
  This summary will replace the full conversation history when the session
@@ -4108,6 +4172,26 @@ function buildResourcesAvailablePayload(manager, mcpManager, driverLive, bakedSe
4108
4172
  }));
4109
4173
  return { type: "resources_available", mounts, connectors, mcp_servers };
4110
4174
  }
4175
+ function isTerminalFlowStatus(status) {
4176
+ return status === "complete" || status === "cancelled";
4177
+ }
4178
+ function hasNonTerminalFlow(flows) {
4179
+ for (const f of flows) {
4180
+ if (!isTerminalFlowStatus(f.adapter.getExecution(f.handle).status)) return true;
4181
+ }
4182
+ return false;
4183
+ }
4184
+ function registerActiveFlow(flows, runId, entry) {
4185
+ const removed = [];
4186
+ for (const [key, existing] of flows) {
4187
+ if (isTerminalFlowStatus(existing.adapter.getExecution(existing.handle).status)) {
4188
+ flows.delete(key);
4189
+ removed.push(key);
4190
+ }
4191
+ }
4192
+ flows.set(runId, entry);
4193
+ return removed;
4194
+ }
4111
4195
  async function buildStoreSnapshotReplay(manager, activeFlows) {
4112
4196
  const events = [];
4113
4197
  for (const { runId, adapter, handle } of activeFlows) {
@@ -4258,10 +4342,10 @@ function planFlowMutate(cmd, ctx) {
4258
4342
  reason: "flow.start payload missing required fields (need payload.flow.id, payload.seed.runId, payload.seed.startedBy)"
4259
4343
  };
4260
4344
  }
4261
- if (ctx.hasActiveFlow) {
4345
+ if (ctx.hasRunningFlow) {
4262
4346
  return {
4263
4347
  kind: "reject",
4264
- reason: "flow.start ignored: a flow is already active (one active flow per session; cancel it first)"
4348
+ reason: "flow.start ignored: a flow is still running (one active flow per session; cancel it, then start the next)"
4265
4349
  };
4266
4350
  }
4267
4351
  return { kind: "create-start", flowDef, seed };
@@ -4817,6 +4901,7 @@ async function startAgentServer(opts) {
4817
4901
  sendEvent(rewriteFileChangedPath(event, opts.projectDir, mounts));
4818
4902
  }
4819
4903
  let driverStarted = false;
4904
+ const driverSwapGate = new DriverSwapGate();
4820
4905
  let driver = secretsMode === "env" ? agentSession.driver : void 0;
4821
4906
  let secretsProvisioned = secretsMode === "env";
4822
4907
  function swapDriver() {
@@ -5012,7 +5097,8 @@ async function startAgentServer(opts) {
5012
5097
  }
5013
5098
  }
5014
5099
  const entry = { runId, flowDef, adapter, handle, connectorId };
5015
- activeFlows.set(runId, entry);
5100
+ const pruned = registerActiveFlow(activeFlows, runId, entry);
5101
+ if (pruned.length > 0) serverLog.debug("pruned terminal flow runs", { runIds: pruned });
5016
5102
  return entry;
5017
5103
  }
5018
5104
  const refreshFlagPath = join(opts.projectDir, ".skaile", "refresh-request");
@@ -5229,6 +5315,20 @@ async function startAgentServer(opts) {
5229
5315
  return { needsWireFallbackRecreate, needsWireAgentConfigRecreate };
5230
5316
  }
5231
5317
  async function recreateAgentSession(overrides) {
5318
+ let markSwapDone;
5319
+ driverSwapGate.beginSwap(
5320
+ new Promise((resolve4) => {
5321
+ markSwapDone = resolve4;
5322
+ })
5323
+ );
5324
+ try {
5325
+ await driverSwapGate.awaitActiveTurn();
5326
+ await recreateAgentSessionInner(overrides);
5327
+ } finally {
5328
+ markSwapDone();
5329
+ }
5330
+ }
5331
+ async function recreateAgentSessionInner(overrides) {
5232
5332
  const disposedManager = agentSession.resourceManager;
5233
5333
  await agentSession.dispose();
5234
5334
  agentSession = await createAgentSession({ ...sessionConfig, ...overrides });
@@ -5561,6 +5661,7 @@ async function startAgentServer(opts) {
5561
5661
  return;
5562
5662
  }
5563
5663
  log(`[serve] prompt: ${cmd.prompt.slice(0, 80)}...`);
5664
+ await driverSwapGate.awaitSwap();
5564
5665
  await restageMaterializedSkills(true);
5565
5666
  const activeFlow = getOnlyActiveFlow();
5566
5667
  if (activeFlow) {
@@ -5610,18 +5711,23 @@ async function startAgentServer(opts) {
5610
5711
  });
5611
5712
  }
5612
5713
  sendEvent({ type: "status", phase: "thinking" });
5714
+ const endTurn = driverSwapGate.beginTurn();
5613
5715
  try {
5614
5716
  await driver.prompt(cmd.prompt);
5615
5717
  } catch (err) {
5616
5718
  const errMsg = err instanceof Error ? err.message : String(err);
5617
5719
  log(`[serve] prompt failed: ${errMsg}`);
5618
5720
  sendEvent({ type: "error", message: `Prompt failed: ${errMsg}`, fatal: true });
5721
+ } finally {
5722
+ endTurn();
5619
5723
  }
5620
5724
  }
5621
5725
  async function handleReply(cmd) {
5622
5726
  if (!await awaitSessionReadyOrFail()) return;
5623
5727
  log(`[serve] reply: ${cmd.answer.slice(0, 80)}...`);
5728
+ await driverSwapGate.awaitSwap();
5624
5729
  if (driver.hasPendingQuestion() && driver.answerQuestion(cmd.answer)) return;
5730
+ const endTurn = driverSwapGate.beginTurn();
5625
5731
  try {
5626
5732
  await driver.prompt(cmd.answer);
5627
5733
  } catch (err) {
@@ -5630,6 +5736,8 @@ async function startAgentServer(opts) {
5630
5736
  message: `Reply failed: ${err instanceof Error ? err.message : String(err)}`,
5631
5737
  fatal: false
5632
5738
  });
5739
+ } finally {
5740
+ endTurn();
5633
5741
  }
5634
5742
  }
5635
5743
  async function handleCancel() {
@@ -5678,6 +5786,7 @@ async function startAgentServer(opts) {
5678
5786
  targetExists: !!targetEntry,
5679
5787
  isExistingFlow,
5680
5788
  hasActiveFlow: activeFlows.size > 0,
5789
+ hasRunningFlow: hasNonTerminalFlow(activeFlows.values()),
5681
5790
  onlyActiveConnectorId: getOnlyActiveFlow()?.connectorId ?? null
5682
5791
  });
5683
5792
  switch (plan.kind) {
@@ -6202,6 +6311,6 @@ function touchSession(state) {
6202
6311
  return { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
6203
6312
  }
6204
6313
 
6205
- export { CLAUDE_CODE_CREDENTIALS_KEY, COMPILE_MANIFEST_FILENAME, CapabilityRegistry, DEFAULT_CAPABILITY_CALL_TIMEOUT_MS, DEFAULT_COALESCE_MS, MarkdownStreamer, PreInitRingSink, agentDefinitionExists, bootstrapCapabilityRegistry, bootstrapRunnerLogStore, buildAgentResources, buildClientCapabilityHandler, buildConnectorTokenMediator, buildContextSection, buildEnvironmentSection, buildResourcesAvailablePayload, buildStoreSnapshotReplay, builtinCapabilities, capabilityLogInstance, clearPreInitRingSink, clearSession, compileComposition, computeCapabilitySignature, connectorRefreshKind, createAgentSession, createSessionStimulusBus, defineCapability, deleteSession, dispatchRunnerCapabilityInvocation, emitSystemPromptComposed, ensureGitConfigInclude, extractClaudeAiOauthExpiresAt, getPreInitRingSink, handleMountResourceRequest, handleResourceRequest, hostOpResumeStimulus, installPreInitRingSink, listSessions, loadAgentManifest, loadCompileManifest, loadCompileManifestFromDir, loadSession, loadSessionById, mcpAuthSecretKey, newSession, pickRepointedManager, planFlowMutate, registerCompositionCapabilities, rejectCapabilityOnApprovalDeny, resetRunnerLogStore, resolveAgentComposition, resolveAgentMixins, resolveBinding, resolveCapabilityCallTimeoutMs, resolveCapabilityResult, resolveComposition, resolveMixin, runAgentChat, saveSession, setCurrentSession, startAgentServer, stimulusFromMetas, touchSession, writeClaudeCodeCredentialsFile };
6206
- //# sourceMappingURL=chunk-SORYPIC3.js.map
6207
- //# sourceMappingURL=chunk-SORYPIC3.js.map
6314
+ export { CLAUDE_CODE_CREDENTIALS_KEY, COMPILE_MANIFEST_FILENAME, CapabilityRegistry, DEFAULT_CAPABILITY_CALL_TIMEOUT_MS, DEFAULT_COALESCE_MS, MarkdownStreamer, PreInitRingSink, agentDefinitionExists, bootstrapCapabilityRegistry, bootstrapRunnerLogStore, buildAgentResources, buildClientCapabilityHandler, buildConnectorTokenMediator, buildContextSection, buildEnvironmentSection, buildResourcesAvailablePayload, buildStoreSnapshotReplay, builtinCapabilities, capabilityLogInstance, clearPreInitRingSink, clearSession, compileComposition, computeCapabilitySignature, connectorRefreshKind, createAgentSession, createSessionStimulusBus, defineCapability, deleteSession, dispatchRunnerCapabilityInvocation, emitSystemPromptComposed, ensureGitConfigInclude, extractClaudeAiOauthExpiresAt, getPreInitRingSink, handleMountResourceRequest, handleResourceRequest, hasNonTerminalFlow, hostOpResumeStimulus, installPreInitRingSink, isTerminalFlowStatus, listSessions, loadAgentManifest, loadCompileManifest, loadCompileManifestFromDir, loadSession, loadSessionById, mcpAuthSecretKey, newSession, pickRepointedManager, planFlowMutate, registerActiveFlow, registerCompositionCapabilities, rejectCapabilityOnApprovalDeny, resetRunnerLogStore, resolveAgentComposition, resolveAgentMixins, resolveBinding, resolveCapabilityCallTimeoutMs, resolveCapabilityResult, resolveComposition, resolveMixin, runAgentChat, saveSession, setCurrentSession, startAgentServer, stimulusFromMetas, touchSession, writeClaudeCodeCredentialsFile };
6315
+ //# sourceMappingURL=chunk-IO3K4PTB.js.map
6316
+ //# sourceMappingURL=chunk-IO3K4PTB.js.map