@linzumi/cli 0.0.87-beta → 0.0.88-beta

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +357 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,7 +58,7 @@ Install the CLI or run it with `npx`:
58
58
  ```bash
59
59
  npm install -g @linzumi/cli@latest
60
60
  npx -y @linzumi/cli@latest signup
61
- npx -y @linzumi/cli@0.0.87-beta --version
61
+ npx -y @linzumi/cli@0.0.88-beta --version
62
62
  linzumi --version
63
63
  ```
64
64
 
package/dist/index.js CHANGED
@@ -11541,7 +11541,7 @@ function createClaudeCodeSessionPipeline(host) {
11541
11541
  const toolName2 = pending?.toolName ?? event.toolName ?? "tool";
11542
11542
  const input = pending?.input ?? {};
11543
11543
  const itemId = `tool:${event.itemKey}`;
11544
- if (!event.isError && isFileChangeTool(toolName2)) {
11544
+ if (isFileChangeTool(toolName2)) {
11545
11545
  const patchText = claudeFileChangePatchText(toolName2, input, {
11546
11546
  targetExistedAtCall: pending?.targetExistedAtCall
11547
11547
  });
@@ -11549,7 +11549,7 @@ function createClaudeCodeSessionPipeline(host) {
11549
11549
  const item2 = {
11550
11550
  type: "fileChange",
11551
11551
  id: itemId,
11552
- status: "completed",
11552
+ status: event.isError ? "failed" : "completed",
11553
11553
  patchText
11554
11554
  };
11555
11555
  recordItem(turn, item2);
@@ -11569,12 +11569,13 @@ function createClaudeCodeSessionPipeline(host) {
11569
11569
  submit("item/completed", { turnId: turn.turnId, item: item2 });
11570
11570
  return;
11571
11571
  }
11572
- if (!event.isError && toolName2 === "TodoWrite") {
11572
+ if (!event.isError && isClaudePlanTool(toolName2)) {
11573
11573
  host.onTodoWriteCompleted?.(input);
11574
11574
  const plan = claudePlanItem(itemId, input);
11575
11575
  if (plan !== void 0) {
11576
- recordItem(turn, plan);
11577
- submit("item/completed", { turnId: turn.turnId, item: plan });
11576
+ const planRow = { ...plan, id: itemId };
11577
+ recordItem(turn, planRow);
11578
+ submit("item/completed", { turnId: turn.turnId, item: planRow });
11578
11579
  return;
11579
11580
  }
11580
11581
  }
@@ -11605,7 +11606,7 @@ function createClaudeCodeSessionPipeline(host) {
11605
11606
  }
11606
11607
  case "reasoning_delta": {
11607
11608
  const turn = ensureTurn();
11608
- const itemId = claudeAssistantPipelineItemId(event.itemKey);
11609
+ const itemId = claudeReasoningPipelineItemId(event.itemKey);
11609
11610
  appendRawText(turn, itemId, "reasoning", event.delta);
11610
11611
  submit("item/reasoning/textDelta", {
11611
11612
  turnId: turn.turnId,
@@ -11626,7 +11627,7 @@ function createClaudeCodeSessionPipeline(host) {
11626
11627
  }
11627
11628
  case "reasoning_message": {
11628
11629
  const turn = ensureTurn();
11629
- const itemId = claudeAssistantPipelineItemId(event.itemKey);
11630
+ const itemId = claudeReasoningPipelineItemId(event.itemKey);
11630
11631
  setRawText(turn, itemId, "reasoning", event.content);
11631
11632
  submit("item/completed", {
11632
11633
  turnId: turn.turnId,
@@ -11636,14 +11637,15 @@ function createClaudeCodeSessionPipeline(host) {
11636
11637
  }
11637
11638
  case "tool_call": {
11638
11639
  const turn = ensureTurn();
11640
+ const targetExistedAtCall = fileChangeTargetExists(
11641
+ event.toolName,
11642
+ event.input,
11643
+ host.cwd
11644
+ );
11639
11645
  turn.pendingTools.set(event.itemKey, {
11640
11646
  toolName: event.toolName,
11641
11647
  input: event.input,
11642
- targetExistedAtCall: fileChangeTargetExists(
11643
- event.toolName,
11644
- event.input,
11645
- host.cwd
11646
- )
11648
+ targetExistedAtCall
11647
11649
  });
11648
11650
  if (isWebSearchTool(event.toolName)) {
11649
11651
  const query = stringValue(event.input.query) ?? stringValue(event.input.url) ?? event.toolName;
@@ -11655,7 +11657,34 @@ function createClaudeCodeSessionPipeline(host) {
11655
11657
  query
11656
11658
  }
11657
11659
  });
11660
+ return;
11661
+ }
11662
+ if (isClaudePlanTool(event.toolName)) {
11663
+ return;
11664
+ }
11665
+ const itemId = `tool:${event.itemKey}`;
11666
+ if (isFileChangeTool(event.toolName)) {
11667
+ const patchText = claudeFileChangePatchText(
11668
+ event.toolName,
11669
+ event.input,
11670
+ { targetExistedAtCall }
11671
+ );
11672
+ if (patchText !== void 0 && patchText.trim() !== "") {
11673
+ submit("item/fileChange/outputDelta", {
11674
+ turnId: turn.turnId,
11675
+ itemId,
11676
+ delta: patchText
11677
+ });
11678
+ }
11679
+ return;
11658
11680
  }
11681
+ submit("item/commandExecution/outputDelta", {
11682
+ turnId: turn.turnId,
11683
+ itemId,
11684
+ command: claudeToolCommandLabel(event.toolName, event.input),
11685
+ stream: "stdout",
11686
+ delta: "\n"
11687
+ });
11659
11688
  return;
11660
11689
  }
11661
11690
  case "tool_result": {
@@ -11809,6 +11838,9 @@ function claudeAssistantPipelineItemId(itemKey) {
11809
11838
  const streamIndex = itemKey.match(/^assistant-stream-(\d+)$/)?.[1];
11810
11839
  return streamIndex === void 0 ? itemKey : `content-block-${streamIndex}`;
11811
11840
  }
11841
+ function claudeReasoningPipelineItemId(itemKey) {
11842
+ return `reasoning-${claudeAssistantPipelineItemId(itemKey)}`;
11843
+ }
11812
11844
  function isFileChangeTool(toolName2) {
11813
11845
  switch (toolName2) {
11814
11846
  case "Edit":
@@ -11823,6 +11855,9 @@ function isFileChangeTool(toolName2) {
11823
11855
  function isWebSearchTool(toolName2) {
11824
11856
  return toolName2 === "WebSearch" || toolName2 === "WebFetch";
11825
11857
  }
11858
+ function isClaudePlanTool(toolName2) {
11859
+ return toolName2 === "TodoWrite" || toolName2 === "TaskCreate" || toolName2 === "TaskUpdate";
11860
+ }
11826
11861
  function fileChangeTargetExists(toolName2, input, sessionCwd) {
11827
11862
  if (!isFileChangeTool(toolName2)) {
11828
11863
  return void 0;
@@ -11895,9 +11930,18 @@ ${body}
11895
11930
  *** End Patch
11896
11931
  `;
11897
11932
  }
11933
+ function claudePlanInputItems(input) {
11934
+ if (Array.isArray(input.todos)) {
11935
+ return input.todos;
11936
+ }
11937
+ if (Array.isArray(input.tasks)) {
11938
+ return input.tasks;
11939
+ }
11940
+ return [];
11941
+ }
11898
11942
  function claudePlanItem(itemId, input) {
11899
- const todos = Array.isArray(input.todos) ? input.todos : [];
11900
- const lines = todos.flatMap((todo) => {
11943
+ const items = claudePlanInputItems(input);
11944
+ const lines = items.flatMap((todo) => {
11901
11945
  const value = objectValue(todo);
11902
11946
  if (value === void 0) {
11903
11947
  return [];
@@ -11971,7 +12015,7 @@ function objectValue2(value) {
11971
12015
  return isJsonObject(value) ? value : void 0;
11972
12016
  }
11973
12017
  function claudeTodoWritePlanSteps(input) {
11974
- const todos = Array.isArray(input.todos) ? input.todos : [];
12018
+ const todos = Array.isArray(input.todos) ? input.todos : Array.isArray(input.tasks) ? input.tasks : [];
11975
12019
  const steps = todos.flatMap((todo) => {
11976
12020
  const value = objectValue2(todo);
11977
12021
  if (value === void 0) {
@@ -12042,6 +12086,29 @@ function createClaudeCodePlanMirror(args) {
12042
12086
  settle: () => flight
12043
12087
  };
12044
12088
  }
12089
+ async function seedClaudeCodingJobGoal(args) {
12090
+ const goal = args.goal.trim().slice(0, 2e3);
12091
+ if (goal === "") {
12092
+ return false;
12093
+ }
12094
+ try {
12095
+ await args.upsertCodingJobPlan(
12096
+ { thread_id: args.threadId, goal },
12097
+ { signal: args.signal }
12098
+ );
12099
+ args.log("claude_code.goal_seeded", {
12100
+ thread_id: args.threadId,
12101
+ goal_length: goal.length
12102
+ });
12103
+ return true;
12104
+ } catch (error) {
12105
+ args.log("claude_code.goal_seed_failed", {
12106
+ thread_id: args.threadId,
12107
+ message: error instanceof Error ? error.message : String(error)
12108
+ });
12109
+ return false;
12110
+ }
12111
+ }
12045
12112
  var todoStatusToPlanStepStatus, planStepTitleMaxLength, planStepDescriptionMaxLength, planStepsMaxCount;
12046
12113
  var init_claudeCodePlanMirror = __esm({
12047
12114
  "src/claudeCodePlanMirror.ts"() {
@@ -12597,6 +12664,87 @@ var init_claudeCodeLiveBashOutput = __esm({
12597
12664
  }
12598
12665
  });
12599
12666
 
12667
+ // src/claudeCodeTurnStallWatchdog.ts
12668
+ function createClaudeCodeTurnStallWatchdog(options) {
12669
+ if (!(options.thresholdMs > 0)) {
12670
+ throw new Error("Claude turn-stall threshold must be > 0");
12671
+ }
12672
+ const now = options.now ?? (() => Date.now());
12673
+ const setTimer = options.setTimer ?? ((callback, delayMs) => {
12674
+ const handle2 = setTimeout(callback, delayMs);
12675
+ handle2.unref?.();
12676
+ return handle2;
12677
+ });
12678
+ const clearTimer = options.clearTimer ?? ((handle2) => clearTimeout(handle2));
12679
+ const isTurnActive = options.isTurnActive ?? (() => true);
12680
+ let handle;
12681
+ let lastKickMs = now();
12682
+ let stopped = false;
12683
+ let fired = false;
12684
+ let wasActive = isTurnActive();
12685
+ const disarm = () => {
12686
+ if (handle !== void 0) {
12687
+ clearTimer(handle);
12688
+ handle = void 0;
12689
+ }
12690
+ };
12691
+ const onTimeout = () => {
12692
+ handle = void 0;
12693
+ if (stopped || fired) {
12694
+ return;
12695
+ }
12696
+ const active = isTurnActive();
12697
+ if (!active) {
12698
+ wasActive = false;
12699
+ arm();
12700
+ return;
12701
+ }
12702
+ if (!wasActive) {
12703
+ wasActive = true;
12704
+ lastKickMs = now();
12705
+ arm();
12706
+ return;
12707
+ }
12708
+ const silentMs = now() - lastKickMs;
12709
+ if (silentMs < options.thresholdMs) {
12710
+ arm();
12711
+ return;
12712
+ }
12713
+ fired = true;
12714
+ options.onStall({ silentMs });
12715
+ };
12716
+ function arm() {
12717
+ if (stopped || fired) {
12718
+ return;
12719
+ }
12720
+ disarm();
12721
+ const delay = isTurnActive() ? Math.max(0, options.thresholdMs - (now() - lastKickMs)) : options.thresholdMs;
12722
+ handle = setTimer(onTimeout, delay);
12723
+ }
12724
+ arm();
12725
+ return {
12726
+ kick: () => {
12727
+ if (stopped || fired) {
12728
+ return;
12729
+ }
12730
+ wasActive = true;
12731
+ lastKickMs = now();
12732
+ arm();
12733
+ },
12734
+ stop: () => {
12735
+ stopped = true;
12736
+ disarm();
12737
+ }
12738
+ };
12739
+ }
12740
+ var defaultClaudeCodeTurnStallThresholdMs;
12741
+ var init_claudeCodeTurnStallWatchdog = __esm({
12742
+ "src/claudeCodeTurnStallWatchdog.ts"() {
12743
+ "use strict";
12744
+ defaultClaudeCodeTurnStallThresholdMs = 10 * 6e4;
12745
+ }
12746
+ });
12747
+
12600
12748
  // src/claudeCodeSession.ts
12601
12749
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
12602
12750
  import { homedir as homedir6 } from "node:os";
@@ -12864,8 +13012,49 @@ async function startClaudeCodeSession(options) {
12864
13012
  lastCompletedTurnBody: void 0
12865
13013
  };
12866
13014
  const streamUsageTracker = createClaudeCodeStreamUsageTracker();
13015
+ let stallReason;
13016
+ let signalStall;
13017
+ const stallSignal = new Promise((resolve12) => {
13018
+ signalStall = resolve12;
13019
+ });
13020
+ const stallWatchdog = options.turnStallThresholdMs !== void 0 && options.turnStallThresholdMs > 0 ? createClaudeCodeTurnStallWatchdog({
13021
+ thresholdMs: options.turnStallThresholdMs,
13022
+ isTurnActive: options.turnStallIsActive,
13023
+ onStall: ({ silentMs }) => {
13024
+ stallReason = `Claude Code turn stalled: no SDK output for ${Math.round(
13025
+ silentMs / 1e3
13026
+ )}s (threshold ${Math.round(
13027
+ (options.turnStallThresholdMs ?? 0) / 1e3
13028
+ )}s)`;
13029
+ options.abortController?.abort();
13030
+ signalStall?.();
13031
+ }
13032
+ }) : void 0;
12867
13033
  try {
12868
- for await (const message of runner(options)) {
13034
+ const iterator = runner(options)[Symbol.asyncIterator]();
13035
+ for (; ; ) {
13036
+ let nextStep;
13037
+ if (stallWatchdog) {
13038
+ const nextPromise = iterator.next().then((result) => ({ kind: "next", result }));
13039
+ nextPromise.catch(() => void 0);
13040
+ const raced = await Promise.race([
13041
+ nextPromise,
13042
+ stallSignal.then(() => ({ kind: "stall" }))
13043
+ ]);
13044
+ if (raced.kind === "stall") {
13045
+ void Promise.resolve(iterator.return?.()).catch(() => void 0);
13046
+ break;
13047
+ }
13048
+ nextStep = raced;
13049
+ } else {
13050
+ nextStep = { kind: "next", result: await iterator.next() };
13051
+ }
13052
+ const step = nextStep;
13053
+ if (step.result.done === true) {
13054
+ break;
13055
+ }
13056
+ const message = step.result.value;
13057
+ stallWatchdog?.kick();
12869
13058
  state.sessionId = state.sessionId ?? extractClaudeSessionId(message);
12870
13059
  const sessionId = extractClaudeSessionId(message) ?? state.sessionId;
12871
13060
  if (sessionId !== void 0 && !state.startedSessionIds.has(sessionId)) {
@@ -12931,8 +13120,12 @@ async function startClaudeCodeSession(options) {
12931
13120
  }
12932
13121
  }
12933
13122
  } finally {
13123
+ stallWatchdog?.stop();
12934
13124
  options.streamingInput?.close();
12935
13125
  }
13126
+ if (stallReason !== void 0) {
13127
+ return await failClaudeCodeSession(options, state.sessionId, stallReason);
13128
+ }
12936
13129
  return completeClaudeCodeSession(options, state);
12937
13130
  }
12938
13131
  async function emitClaudeCodeTurnCompleted(options, state) {
@@ -13325,13 +13518,16 @@ function assistantTranscriptEvents(message, sessionId) {
13325
13518
  contentBlockItemKey(block, index)
13326
13519
  )
13327
13520
  );
13328
- const directText = nonEmptyText(stringValue(message?.text));
13521
+ const hasTextContentBlock = content.some(
13522
+ (block) => stringValue(objectValue(block)?.type) === "text"
13523
+ );
13524
+ const directText = hasTextContentBlock ? void 0 : nonEmptyText(stringValue(message?.text));
13329
13525
  return directText === void 0 ? contentEvents : [
13330
13526
  ...contentEvents,
13331
13527
  {
13332
13528
  type: "assistant_message",
13333
13529
  sessionId,
13334
- itemKey: stringValue(message?.uuid) ?? "assistant-message",
13530
+ itemKey: `content-block-${content.length}`,
13335
13531
  content: directText,
13336
13532
  streamState: "completed"
13337
13533
  }
@@ -13352,7 +13548,7 @@ function userTranscriptEvents(message, sessionId) {
13352
13548
  {
13353
13549
  type: "tool_result",
13354
13550
  sessionId,
13355
- itemKey: contentBlockItemKey(block, index),
13551
+ itemKey: stringValue(value?.tool_use_id) ?? stringValue(value?.id) ?? `content-block-${index}`,
13356
13552
  toolName: void 0,
13357
13553
  content: visibleToolResultContent(value),
13358
13554
  isError: value?.is_error === true
@@ -13438,12 +13634,14 @@ function unknownTranscriptEvent(message, sessionId) {
13438
13634
  }
13439
13635
  function streamItemKey(event, _message) {
13440
13636
  const index = integerValue(event?.index);
13441
- const contentBlock = objectValue(event?.content_block);
13442
- return stringValue(event?.content_block_id) ?? stringValue(contentBlock?.id) ?? stringValue(contentBlock?.tool_use_id) ?? stringValue(contentBlock?.uuid) ?? (index === void 0 ? "assistant-stream" : `assistant-stream-${index}`);
13637
+ return index === void 0 ? "assistant-stream" : `content-block-${index}`;
13443
13638
  }
13444
13639
  function contentBlockItemKey(block, index) {
13445
13640
  const value = objectValue(block);
13446
- return stringValue(value?.id) ?? stringValue(value?.tool_use_id) ?? stringValue(value?.uuid) ?? `content-block-${index}`;
13641
+ if (stringValue(value?.type) === "tool_use") {
13642
+ return stringValue(value?.id) ?? stringValue(value?.tool_use_id) ?? `content-block-${index}`;
13643
+ }
13644
+ return `content-block-${index}`;
13447
13645
  }
13448
13646
  function visibleToolResultContent(block) {
13449
13647
  const content = block?.content;
@@ -13491,6 +13689,7 @@ var init_claudeCodeSession = __esm({
13491
13689
  "use strict";
13492
13690
  init_json();
13493
13691
  init_claudeCodeLiveBashOutput();
13692
+ init_claudeCodeTurnStallWatchdog();
13494
13693
  claudeRateLimitWindowLabels = {
13495
13694
  five_hour: "5h window",
13496
13695
  seven_day: "7d window",
@@ -18520,7 +18719,7 @@ var linzumiCliVersion, linzumiCliVersionText;
18520
18719
  var init_version = __esm({
18521
18720
  "src/version.ts"() {
18522
18721
  "use strict";
18523
- linzumiCliVersion = "0.0.87-beta";
18722
+ linzumiCliVersion = "0.0.88-beta";
18524
18723
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
18525
18724
  }
18526
18725
  });
@@ -20909,7 +21108,7 @@ function createLinzumiMcpApiClient(options) {
20909
21108
  const baseUrl = kandanHttpBaseUrl(options.kandanUrl);
20910
21109
  const apiPrefix = options.authMode === "personal-agent-delegation" ? "/api/v2/personal-agent-mcp" : "/api/v2/local-runner-mcp";
20911
21110
  const operatingMode = options.operatingMode ?? "text";
20912
- const request = async (method, path2, params) => {
21111
+ const request = async (method, path2, params, requestOptions = {}) => {
20913
21112
  const url = new URL(path2, baseUrl);
20914
21113
  const paramsWithMode = {
20915
21114
  ...params,
@@ -20917,7 +21116,11 @@ function createLinzumiMcpApiClient(options) {
20917
21116
  };
20918
21117
  const requestInit = {
20919
21118
  method,
20920
- headers: { authorization: `Bearer ${options.accessToken}` }
21119
+ headers: { authorization: `Bearer ${options.accessToken}` },
21120
+ // An aborted signal cancels the in-flight fetch so a caller that has
21121
+ // already given up (e.g. a timed-out goal seed) cannot land a LATE POST
21122
+ // and clobber state written after it gave up (#1864 finding 2).
21123
+ ...requestOptions.signal === void 0 ? {} : { signal: requestOptions.signal }
20921
21124
  };
20922
21125
  if (method === "GET") {
20923
21126
  for (const [key, value] of Object.entries(paramsWithMode)) {
@@ -20950,9 +21153,11 @@ function createLinzumiMcpApiClient(options) {
20950
21153
  getMessage: (params) => request("GET", `${apiPrefix}/message`, params),
20951
21154
  getThread: (params) => request("GET", `${apiPrefix}/thread`, params),
20952
21155
  getChannel: (params) => request("GET", `${apiPrefix}/channel`, params),
21156
+ searchWorkspace: (params) => request("GET", `${apiPrefix}/search`, params),
21157
+ askWorkspace: (params) => request("POST", `${apiPrefix}/ask`, params),
20953
21158
  getCodingJobMetadata: (params) => request("GET", `${apiPrefix}/coding-job-metadata`, params),
20954
21159
  renameCodingJob: (params) => request("POST", `${apiPrefix}/coding-job-title`, params),
20955
- upsertCodingJobPlan: (params) => request("POST", `${apiPrefix}/coding-job-plan`, params),
21160
+ upsertCodingJobPlan: (params, requestOptions) => request("POST", `${apiPrefix}/coding-job-plan`, params, requestOptions),
20956
21161
  replaceCodingJobPlanSteps: (params) => request("POST", `${apiPrefix}/coding-job-plan/steps`, params),
20957
21162
  updateCodingJobPlanStep: (params) => request("POST", `${apiPrefix}/coding-job-plan/step`, params),
20958
21163
  linkCodingJobPullRequest: (params) => request("POST", `${apiPrefix}/coding-job-primary-pr`, params),
@@ -25879,12 +26084,18 @@ function truncateFailureDetail(message) {
25879
26084
  }
25880
26085
  function commanderDeveloperInstructions(args) {
25881
26086
  const agentLabel = args.agentLabel ?? "Codex";
25882
- const planStepsInstruction = args.planTool === "todo-write" ? `For coding-job threads, start by setting the current goal with
25883
- linzumi_upsert_coding_job_plan, then record the plan steps with the TodoWrite
25884
- tool. Linzumi mirrors your TodoWrite list into the coding-job plan steps
25885
- automatically, one TodoWrite item per plan step.` : `For coding-job threads, start by setting the current goal with
25886
- linzumi_upsert_coding_job_plan, then replace the plan steps with
25887
- linzumi_replace_coding_job_plan_steps.`;
26087
+ const planOpener = `Before you begin ANY work - and again whenever the job
26088
+ changes - call linzumi_upsert_coding_job_plan to set or refresh the current
26089
+ goal and the high-level plan. Always record the plan up front, before starting
26090
+ work, so the user can see a high-level view of what you are doing at all times
26091
+ and trust your work. Keep it accurate as you go: whenever you discover steps to
26092
+ add, steps that are no longer needed, or the nature of the job changes, update
26093
+ it with linzumi_upsert_coding_job_plan.`;
26094
+ const planStepsInstruction = args.planTool === "todo-write" ? `${planOpener}
26095
+ Record and maintain the plan STEPS with the TodoWrite tool. Linzumi mirrors
26096
+ your TodoWrite list into the coding-job plan steps automatically, one TodoWrite
26097
+ item per plan step.` : `${planOpener}
26098
+ Replace the plan STEPS with linzumi_replace_coding_job_plan_steps.`;
25888
26099
  const planUpdateInstruction = args.planTool === "todo-write" ? `As work proceeds, update each step's status with TodoWrite. Do not call
25889
26100
  linzumi_replace_coding_job_plan_steps or linzumi_update_coding_job_plan_step
25890
26101
  directly; TodoWrite is the source of truth for the plan steps.` : `As work proceeds, update each step with linzumi_update_coding_job_plan_step.`;
@@ -26273,6 +26484,14 @@ async function startCodexProviderInstance(args) {
26273
26484
  }
26274
26485
  };
26275
26486
  }
26487
+ function claudeCodeTurnStallThresholdMs(env) {
26488
+ const raw = env.LINZUMI_CLAUDE_TURN_STALL_MS?.trim();
26489
+ if (raw === void 0 || raw === "") {
26490
+ return defaultClaudeCodeTurnStallThresholdMs;
26491
+ }
26492
+ const parsed = Number.parseInt(raw, 10);
26493
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultClaudeCodeTurnStallThresholdMs;
26494
+ }
26276
26495
  function createClaudeCodeInputQueue(input) {
26277
26496
  const pendingMessages = [
26278
26497
  claudeCodeUserInputMessage(input.content)
@@ -26644,12 +26863,47 @@ async function startClaudeCodeProviderInstance(args) {
26644
26863
  fetchImpl: args.options.fetch,
26645
26864
  operatingMode: "text"
26646
26865
  });
26866
+ const seededGoal = workDescription.trim().slice(0, 2e3);
26647
26867
  const planMirror = createClaudeCodePlanMirror({
26648
26868
  replacePlanSteps: planMirrorClient.replaceCodingJobPlanSteps,
26649
26869
  threadId,
26650
- goal: workDescription.trim().slice(0, 2e3),
26870
+ goal: seededGoal,
26651
26871
  log: args.log
26652
26872
  });
26873
+ const seedCodingJobGoalBeforeTurn = async () => {
26874
+ if (args.resumeSessionId !== void 0) {
26875
+ return;
26876
+ }
26877
+ const seedTimeoutMs = 5e3;
26878
+ const seedAbort = new AbortController();
26879
+ let timeoutHandle;
26880
+ const timedOut = new Promise((resolve12) => {
26881
+ timeoutHandle = setTimeout(() => {
26882
+ seedAbort.abort(new Error("claude goal seed timed out"));
26883
+ args.log("claude_code.goal_seed_timeout", {
26884
+ thread_id: threadId,
26885
+ timeout_ms: seedTimeoutMs
26886
+ });
26887
+ resolve12();
26888
+ }, seedTimeoutMs);
26889
+ });
26890
+ try {
26891
+ await Promise.race([
26892
+ seedClaudeCodingJobGoal({
26893
+ upsertCodingJobPlan: planMirrorClient.upsertCodingJobPlan,
26894
+ threadId,
26895
+ goal: seededGoal,
26896
+ log: args.log,
26897
+ signal: seedAbort.signal
26898
+ }),
26899
+ timedOut
26900
+ ]);
26901
+ } finally {
26902
+ if (timeoutHandle !== void 0) {
26903
+ clearTimeout(timeoutHandle);
26904
+ }
26905
+ }
26906
+ };
26653
26907
  const adapter = createClaudeCodeSessionPipeline({
26654
26908
  instanceId: args.instanceId,
26655
26909
  cwd: args.cwd,
@@ -26955,6 +27209,7 @@ async function startClaudeCodeProviderInstance(args) {
26955
27209
  });
26956
27210
  const runSession = async () => {
26957
27211
  let result2;
27212
+ await seedCodingJobGoalBeforeTurn();
26958
27213
  try {
26959
27214
  try {
26960
27215
  result2 = await startClaudeCodeSession({
@@ -26983,6 +27238,20 @@ async function startClaudeCodeProviderInstance(args) {
26983
27238
  resumeSessionId: args.resumeSessionId,
26984
27239
  abortController,
26985
27240
  streamingInput: inputQueue,
27241
+ // PROD-RESILIENCE turn-stall watchdog (real-SDK sessions only; mock
27242
+ // runners drive their own timing). A turn that stays in flight with
27243
+ // no SDK output past the threshold is a wedged SILENT subprocess: the
27244
+ // watchdog aborts it and fails the turn so the thread reconnects,
27245
+ // instead of sitting "processing" forever (the ~28-min-hang incident).
27246
+ // The in-flight predicate (currentSourceSeq() is defined only while a
27247
+ // turn is queued/active) keeps a keep-alive session idling between
27248
+ // turns from being a false positive.
27249
+ ...args.options.claudeCodeRunner === void 0 ? {
27250
+ turnStallThresholdMs: claudeCodeTurnStallThresholdMs(
27251
+ process.env
27252
+ ),
27253
+ turnStallIsActive: () => inputQueue.currentSourceSeq() !== void 0
27254
+ } : {},
26986
27255
  runner: args.options.claudeCodeRunner,
26987
27256
  canUseTool,
26988
27257
  ...mcpServers === void 0 ? {} : { mcpServers },
@@ -29343,6 +29612,7 @@ var init_runner = __esm({
29343
29612
  init_engineChildReaper();
29344
29613
  init_claudeCodeLiveBashOutput();
29345
29614
  init_claudeCodeSession();
29615
+ init_claudeCodeTurnStallWatchdog();
29346
29616
  init_codexAppServer();
29347
29617
  init_codexProjectTrust();
29348
29618
  init_codexRuntimeOptions();
@@ -66096,6 +66366,60 @@ async function runMcpServer(args) {
66096
66366
  },
66097
66367
  async (params) => mcpJsonResult(await client.getChannel(params))
66098
66368
  );
66369
+ server.tool(
66370
+ "linzumi_search_workspace",
66371
+ "Search the Linzumi workspace with hybrid lexical+semantic retrieval, scoped to what the authorizing user can access. Returns scored content chunks with provenance (channel, thread, authored time) and a permalink to cite.",
66372
+ {
66373
+ workspace: external_exports2.string().optional().describe(
66374
+ "Workspace slug. Omit to use the authenticated token scope."
66375
+ ),
66376
+ query: external_exports2.string().min(1).describe("Natural-language or keyword query to retrieve against."),
66377
+ limit: external_exports2.number().int().min(1).max(25).optional().describe("Maximum results to return (default 10, server-capped)."),
66378
+ source_types: external_exports2.array(
66379
+ external_exports2.enum([
66380
+ "message",
66381
+ "coding_job",
66382
+ "github_pr",
66383
+ "github_pr_review",
66384
+ "github_check_run",
66385
+ "github_workflow_run",
66386
+ "github_diff",
66387
+ "dictation",
66388
+ "file"
66389
+ ])
66390
+ ).optional().describe("Restrict results to these corpora; omit for all."),
66391
+ authored_after: external_exports2.string().optional().describe(
66392
+ "ISO8601 instant; only content authored at or after this time."
66393
+ ),
66394
+ authored_before: external_exports2.string().optional().describe(
66395
+ "ISO8601 instant; only content authored at or before this time."
66396
+ ),
66397
+ repo: external_exports2.string().optional().describe(
66398
+ "Restrict GitHub-sourced results to one repository by full name (owner/name)."
66399
+ ),
66400
+ check_status: external_exports2.enum(["pending", "success", "failed"]).optional().describe(
66401
+ "Restrict check and workflow results to one conclusion bucket."
66402
+ )
66403
+ },
66404
+ async (params) => mcpJsonResult(await client.searchWorkspace(params))
66405
+ );
66406
+ server.tool(
66407
+ "linzumi_ask_workspace",
66408
+ 'Ask the Linzumi workspace a natural-language question and get a synthesized, citation-backed answer over what the authorizing user can access. Costs an LLM call on cache misses, so prefer linzumi_search_workspace for simple lookups. A "refused" status means there was not enough accessible evidence - treat it as "the workspace does not know", never retry verbatim.',
66409
+ {
66410
+ workspace: external_exports2.string().optional().describe(
66411
+ "Workspace slug. Omit to use the authenticated token scope."
66412
+ ),
66413
+ question: external_exports2.string().min(1).max(2e3).describe("Natural-language question to answer with citations."),
66414
+ channel: external_exports2.string().optional().describe(
66415
+ "Optional channel slug providing conversational context; never widens access."
66416
+ ),
66417
+ allow_cached: external_exports2.boolean().optional().describe(
66418
+ "Allow serving a cached synthesis when the evidence set is unchanged (default true). Set false to force fresh synthesis."
66419
+ )
66420
+ },
66421
+ async (params) => mcpJsonResult(await client.askWorkspace(params))
66422
+ );
66099
66423
  server.tool(
66100
66424
  "linzumi_get_coding_job_metadata",
66101
66425
  "Read the active coding job workflow metadata for a thread, including the agent-owned goal, ordered plan steps, step lock versions, and workflow status.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.87-beta",
3
+ "version": "0.0.88-beta",
4
4
  "description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {