nolo-cli 0.1.22 → 0.1.23

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 (71) hide show
  1. package/agent-runtime/agentRecordConfig.ts +4 -0
  2. package/agent-runtime/hostAdapter.ts +2 -0
  3. package/agent-runtime/index.ts +7 -0
  4. package/agent-runtime/localLoop.ts +2 -0
  5. package/agent-runtime/platformChatProvider.ts +3 -0
  6. package/agent-runtime/runtimeToolPolicy.ts +92 -0
  7. package/agent-runtime/types.ts +42 -0
  8. package/agentRunCommand.ts +74 -1
  9. package/agentRuntimeCommands.ts +17 -89
  10. package/ai/agent/streamAgentChatTurn.ts +104 -20
  11. package/ai/chat/fetchUtils.native.ts +2 -0
  12. package/ai/chat/fetchUtils.ts +2 -0
  13. package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
  14. package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
  15. package/ai/llm/kimi.ts +1 -1
  16. package/ai/llm/providers.ts +3 -0
  17. package/ai/llm/reasoningModels.ts +1 -0
  18. package/ai/taskRun/taskRunProtocol.ts +1 -0
  19. package/ai/tools/agent/agentTools.ts +17 -0
  20. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  21. package/ai/tools/modelUsageTools.ts +5 -0
  22. package/client/agentRun.test.ts +207 -5
  23. package/client/agentRun.ts +77 -34
  24. package/client/localRuntimeAdapter.test.ts +2 -0
  25. package/client/localRuntimeAdapter.ts +15 -2
  26. package/database/actions/common.ts +4 -3
  27. package/database/config.ts +19 -0
  28. package/machineCommands.ts +400 -45
  29. package/package.json +4 -2
  30. package/render/canvas/canvasEditContext.ts +127 -0
  31. package/render/canvas/canvasRuntime.ts +57 -0
  32. package/render/canvas/canvasSnapshotParser.ts +76 -0
  33. package/render/canvas/canvasTree.ts +308 -0
  34. package/render/canvas/types.ts +46 -0
  35. package/render/layout/deleteBehavior.ts +52 -0
  36. package/render/layout/mainLayoutSidebar.ts +17 -0
  37. package/render/layout/mainLayoutViewMode.ts +56 -0
  38. package/render/layout/topbarUtils.ts +87 -0
  39. package/render/layout/useDevReloadPending.ts +30 -0
  40. package/render/page/createPageAction.ts +183 -0
  41. package/render/page/docSlice.ts +468 -0
  42. package/render/page/server/createPage.ts +174 -0
  43. package/render/page/server/handleCreatePage.ts +91 -0
  44. package/render/page/server/index.ts +4 -0
  45. package/render/page/types.ts +17 -0
  46. package/render/page/useKeyboardSave.ts +48 -0
  47. package/render/styles/zIndex.ts +12 -0
  48. package/render/surf/WeatherIconStyles.ts +17 -0
  49. package/render/surf/color.ts +9 -0
  50. package/render/surf/config.ts +46 -0
  51. package/render/surf/screens/style.ts +1 -0
  52. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  53. package/render/surf/utils/groupedWeatherData.ts +32 -0
  54. package/render/surf/weatherUtils.ts +50 -0
  55. package/render/table/activityColumns.ts +6 -0
  56. package/render/table/createTableAction.ts +270 -0
  57. package/render/table/deleteTableAction.ts +129 -0
  58. package/render/table/fetchAndCacheTableRows.ts +174 -0
  59. package/render/table/tableSlice.ts +1106 -0
  60. package/render/table/tableView.ts +289 -0
  61. package/render/table/toolValueUtils.ts +363 -0
  62. package/render/table/types.ts +252 -0
  63. package/render/table/useCreateTable.ts +72 -0
  64. package/render/table/useTable.ts +61 -0
  65. package/render/table/utils/tableSerialization.ts +50 -0
  66. package/render/web/elements/artifactPreviewCode.ts +43 -0
  67. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  68. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  69. package/render/web/elements/mermaidPreview.ts +21 -0
  70. package/render/web/ui/useInlineEdit.ts +135 -0
  71. package/tableCommands.ts +42 -5
@@ -132,6 +132,12 @@ describe("cli agent run client", () => {
132
132
  expect(requests[0]?.body.userInput).toContain("rowDbKey: row-b2e06f801f-01TASK");
133
133
  expect(requests[0]?.body.userInput).toContain("workItemId: frontend-filter");
134
134
  expect(requests[0]?.body.userInput).toContain("User task:\nFix the filter UI");
135
+ expect(requests[0]?.body.runtimeContext.taskRun).toEqual({
136
+ rowDbKey: "row-b2e06f801f-01TASK",
137
+ taskRunId: "taskrun-1",
138
+ workItemId: "frontend-filter",
139
+ artifactIds: ["artifact-1"],
140
+ });
135
141
  });
136
142
 
137
143
  test("records task-run completion after an HTTP agent run returns a dialog", async () => {
@@ -243,6 +249,42 @@ describe("cli agent run client", () => {
243
249
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
244
250
  });
245
251
 
252
+ test("records background task-run handoff as running instead of completed", async () => {
253
+ const output = new CaptureOutput();
254
+ const writebacks: any[] = [];
255
+
256
+ const result = await runAgentTurn({
257
+ agentName: "frontend-implementer",
258
+ agentKey: "agent-frontend",
259
+ serverUrl: "https://nolo.chat",
260
+ message: "Fix settings style FOUC",
261
+ scriptDir: "C:/missing/scripts",
262
+ env: { AUTH_TOKEN: "token-123" },
263
+ runtimeMode: "server",
264
+ background: true,
265
+ noStream: true,
266
+ taskRunContext: {
267
+ rowDbKey: "row-b2e06f801f-01TASK",
268
+ workItemId: "self",
269
+ },
270
+ taskRunRecorder: async (args) => {
271
+ writebacks.push(args);
272
+ },
273
+ output,
274
+ scriptPathExists: () => false,
275
+ fetchImpl: async () => {
276
+ return Response.json({ dialogId: "dialog-bg", status: "pending" }, { status: 202 });
277
+ },
278
+ });
279
+
280
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-bg" });
281
+ expect(writebacks).toHaveLength(1);
282
+ expect(writebacks[0]).toMatchObject({
283
+ status: "running",
284
+ dialogId: "dialog-bg",
285
+ });
286
+ });
287
+
246
288
  test("runs forced local turns through the injected runtime adapter without HTTP", async () => {
247
289
  const output = new CaptureOutput();
248
290
  const result = await runAgentTurn({
@@ -522,6 +564,58 @@ describe("cli agent run client", () => {
522
564
  });
523
565
  });
524
566
 
567
+ test("records local background task-run handoff as running instead of completed", async () => {
568
+ const output = new CaptureOutput();
569
+ const writebacks: any[] = [];
570
+
571
+ const result = await runAgentTurn({
572
+ agentName: "frontend",
573
+ agentKey: "frontend-local",
574
+ serverUrl: "https://nolo.chat",
575
+ message: "inspect repo in background",
576
+ scriptDir: "C:/missing/scripts",
577
+ env: { AUTH_TOKEN: "token-123" },
578
+ output,
579
+ runtimeMode: "local",
580
+ background: true,
581
+ taskRunContext: {
582
+ rowDbKey: "row-b2e06f801f-01TASK",
583
+ workItemId: "self",
584
+ },
585
+ taskRunRecorder: async (args) => {
586
+ writebacks.push(args);
587
+ },
588
+ localRuntimeAdapter: {
589
+ host: "cli",
590
+ capabilities: ["local-provider", "local-persistence"],
591
+ loadAgentConfig: async (agentRef) => ({
592
+ key: agentRef,
593
+ name: "Frontend",
594
+ prompt: "Fix UI",
595
+ model: "fake-local",
596
+ }),
597
+ loadDialogHistory: async () => [],
598
+ saveTurn: async () => ({ dialogId: "dialog-local-bg" }),
599
+ resolveProvider: async () => ({
600
+ model: "fake-local",
601
+ complete: async () => ({ content: "accepted locally", model: "fake-local" }),
602
+ }),
603
+ executeTool: async () => {
604
+ throw new Error("no tools expected");
605
+ },
606
+ },
607
+ scriptPathExists: () => false,
608
+ });
609
+
610
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-bg" });
611
+ expect(writebacks).toHaveLength(1);
612
+ expect(writebacks[0]).toMatchObject({
613
+ status: "running",
614
+ dialogId: "dialog-local-bg",
615
+ });
616
+ expect(writebacks[0].resultSummary).toBeUndefined();
617
+ });
618
+
525
619
  test("auto mode prefers a working local runtime before HTTP", async () => {
526
620
  const output = new CaptureOutput();
527
621
  const httpCalls: string[] = [];
@@ -616,6 +710,58 @@ describe("cli agent run client", () => {
616
710
  expect(output.text()).toContain("pm > server ok");
617
711
  });
618
712
 
713
+ test("auto mode skips local runtime for server platform tools declared by runtime policy", async () => {
714
+ const output = new CaptureOutput();
715
+ const httpCalls: Array<{ url: string; body: any }> = [];
716
+
717
+ const result = await runAgentTurn({
718
+ agentName: "pm",
719
+ agentKey: "agent-policy-platform-tools",
720
+ serverUrl: "https://nolo.chat",
721
+ message: "query task rows",
722
+ scriptDir: "C:/missing/scripts",
723
+ env: { AUTH_TOKEN: "token-123" },
724
+ output,
725
+ runtimeMode: "auto",
726
+ localRuntimeAdapter: {
727
+ host: "cli",
728
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
729
+ loadAgentConfig: async (agentRef) => ({
730
+ key: agentRef,
731
+ name: "PM",
732
+ prompt: "Manage task rows",
733
+ model: "fake-local",
734
+ runtimeToolPolicy: {
735
+ version: 1,
736
+ agentTools: ["queryTableRows", "taskRun"],
737
+ runtimeTools: ["execShell"],
738
+ },
739
+ }),
740
+ loadDialogHistory: async () => [],
741
+ saveTurn: async () => {
742
+ throw new Error("local runtime should be skipped");
743
+ },
744
+ resolveProvider: async () => {
745
+ throw new Error("local provider should be skipped");
746
+ },
747
+ executeTool: async () => {
748
+ throw new Error("local tools should be skipped");
749
+ },
750
+ },
751
+ scriptPathExists: () => false,
752
+ fetchImpl: async (url, init) => {
753
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
754
+ return Response.json({ content: "server ok", dialogId: "dialog-server-policy" });
755
+ },
756
+ });
757
+
758
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server-policy" });
759
+ expect(httpCalls).toHaveLength(1);
760
+ expect(httpCalls[0]?.body.agentKey).toBe("agent-policy-platform-tools");
761
+ expect(output.text()).toContain("auto runtime: skipping local runtime");
762
+ expect(output.text()).toContain("queryTableRows, taskRun");
763
+ });
764
+
619
765
  test("auto mode skips known platform agents when local config cannot be read", async () => {
620
766
  const output = new CaptureOutput();
621
767
  const httpCalls: Array<{ url: string; body: any }> = [];
@@ -733,11 +879,67 @@ describe("cli agent run client", () => {
733
879
  });
734
880
 
735
881
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-stream" });
736
- expect(output.text()).toContain("nolo -> working");
737
- expect(output.text()).toContain("nolo > 你好");
738
- });
739
-
740
- test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
882
+ expect(output.text()).toContain("nolo -> working");
883
+ expect(output.text()).toContain("nolo > 你好");
884
+ });
885
+
886
+ test("returns a recoverable dialog when a server stream drops after dialog creation", async () => {
887
+ const output = new CaptureOutput();
888
+
889
+ const result = await runAgentTurn({
890
+ agentName: "nolo",
891
+ agentKey: "agent-pub-test",
892
+ serverUrl: "https://nolo.chat",
893
+ message: "hello",
894
+ scriptDir: "C:/missing/scripts",
895
+ env: { AUTH_TOKEN: "token-123" },
896
+ output,
897
+ scriptPathExists: () => false,
898
+ fetchImpl: async () => {
899
+ let sent = false;
900
+ return new Response(
901
+ new ReadableStream({
902
+ pull(controller) {
903
+ const encoder = new TextEncoder();
904
+ if (!sent) {
905
+ sent = true;
906
+ controller.enqueue(
907
+ encoder.encode(
908
+ [
909
+ `data: ${JSON.stringify({
910
+ type: "dialog",
911
+ dialogId: "dialog-recoverable",
912
+ status: "running",
913
+ })}`,
914
+ "",
915
+ `data: ${JSON.stringify({ type: "text", content: "partial" })}`,
916
+ "",
917
+ ].join("\n")
918
+ )
919
+ );
920
+ return;
921
+ }
922
+ controller.error(new Error("socket closed"));
923
+ },
924
+ }),
925
+ {
926
+ status: 200,
927
+ headers: { "Content-Type": "text/event-stream" },
928
+ }
929
+ );
930
+ },
931
+ });
932
+
933
+ expect(result).toEqual({
934
+ exitCode: 0,
935
+ dialogId: "dialog-recoverable",
936
+ streamInterrupted: true,
937
+ });
938
+ expect(output.text()).toContain("dialog dialog-recoverable was created");
939
+ expect(output.text()).toContain("read the dialog before retrying");
940
+ });
941
+
942
+ test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
741
943
  const output = new CaptureOutput();
742
944
 
743
945
  const result = await runAgentTurn({
@@ -59,7 +59,7 @@ type RunAgentTurnOptions = {
59
59
  taskRunContext?: TaskRunPromptContext;
60
60
  taskRunRecorder?: (args: {
61
61
  options: RunAgentTurnOptions;
62
- status: "completed" | "failed";
62
+ status: "running" | "completed" | "failed";
63
63
  dialogId?: string;
64
64
  resultSummary?: string;
65
65
  errorCode?: string;
@@ -69,10 +69,11 @@ type RunAgentTurnOptions = {
69
69
  fetchImpl?: typeof fetch;
70
70
  };
71
71
 
72
- export type RunAgentTurnResult = {
73
- exitCode: number;
74
- dialogId?: string;
75
- };
72
+ export type RunAgentTurnResult = {
73
+ exitCode: number;
74
+ dialogId?: string;
75
+ streamInterrupted?: boolean;
76
+ };
76
77
 
77
78
  type ScriptBridgeDecision = {
78
79
  hasAuthToken: boolean;
@@ -132,6 +133,15 @@ export function findServerPlatformTools(toolNames?: string[]) {
132
133
  return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
133
134
  }
134
135
 
136
+ function resolveServerPlatformToolNames(agentConfig: any) {
137
+ return findServerPlatformTools([
138
+ ...(Array.isArray(agentConfig?.toolNames) ? agentConfig.toolNames : []),
139
+ ...(Array.isArray(agentConfig?.runtimeToolPolicy?.agentTools)
140
+ ? agentConfig.runtimeToolPolicy.agentTools
141
+ : []),
142
+ ]);
143
+ }
144
+
135
145
  function normalizeAgentRef(ref?: string) {
136
146
  return ref?.trim().toLowerCase().replace(/\s+/g, " ");
137
147
  }
@@ -190,7 +200,7 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
190
200
  } catch {
191
201
  return false;
192
202
  }
193
- const serverTools = findServerPlatformTools(agentConfig?.toolNames);
203
+ const serverTools = resolveServerPlatformToolNames(agentConfig);
194
204
  if (serverTools.length === 0) return false;
195
205
  options.output.write(
196
206
  `[nolo] auto runtime: skipping local runtime because ${options.agentKey} declares server platform tools ` +
@@ -348,7 +358,7 @@ async function runTaskRunCli(args: {
348
358
 
349
359
  async function defaultTaskRunRecorder(args: {
350
360
  options: RunAgentTurnOptions;
351
- status: "completed" | "failed";
361
+ status: "running" | "completed" | "failed";
352
362
  dialogId?: string;
353
363
  resultSummary?: string;
354
364
  errorCode?: string;
@@ -357,7 +367,7 @@ async function defaultTaskRunRecorder(args: {
357
367
  const context = args.options.taskRunContext;
358
368
  if (!context?.rowDbKey) return;
359
369
  const role = inferTaskRunAgentRole(args.options.agentName || args.options.agentKey);
360
- if (role === "reviewer") {
370
+ if (role === "reviewer" && args.status !== "running") {
361
371
  await runTaskRunCli({
362
372
  options: args.options,
363
373
  cliArgs: [
@@ -415,7 +425,7 @@ async function defaultTaskRunRecorder(args: {
415
425
  ],
416
426
  });
417
427
  }
418
- if (role === "reviewer") {
428
+ if (role === "reviewer" && args.status !== "running") {
419
429
  const reviewStatus =
420
430
  args.status === "failed" ? "blocked" : classifyTaskRunReviewStatus(args.resultSummary);
421
431
  if (reviewStatus) {
@@ -441,7 +451,7 @@ async function defaultTaskRunRecorder(args: {
441
451
 
442
452
  async function recordTaskRunOutcome(args: {
443
453
  options: RunAgentTurnOptions;
444
- status: "completed" | "failed";
454
+ status: "running" | "completed" | "failed";
445
455
  dialogId?: string;
446
456
  resultSummary?: string;
447
457
  errorCode?: string;
@@ -566,12 +576,28 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
566
576
  options.imageUrls
567
577
  ),
568
578
  runtimeContext: {
569
- surface: "cli",
570
- host: "terminal",
571
- runtime: "bun",
572
- entrypoint: "nolo-cli",
573
- capabilities: ["text-io", "streaming", "slash-commands"],
574
- },
579
+ surface: "cli",
580
+ host: "terminal",
581
+ runtime: "bun",
582
+ entrypoint: "nolo-cli",
583
+ capabilities: ["text-io", "streaming", "slash-commands"],
584
+ ...(options.taskRunContext
585
+ ? {
586
+ taskRun: {
587
+ rowDbKey: options.taskRunContext.rowDbKey,
588
+ ...(options.taskRunContext.taskRunId
589
+ ? { taskRunId: options.taskRunContext.taskRunId }
590
+ : {}),
591
+ ...(options.taskRunContext.workItemId
592
+ ? { workItemId: options.taskRunContext.workItemId }
593
+ : {}),
594
+ ...(options.taskRunContext.artifactIds?.length
595
+ ? { artifactIds: options.taskRunContext.artifactIds }
596
+ : {}),
597
+ },
598
+ }
599
+ : {}),
600
+ },
575
601
  ...(options.continueDialogId
576
602
  ? { continueDialogId: options.continueDialogId }
577
603
  : {}),
@@ -597,7 +623,11 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
597
623
  const result = await readStreamingAgentRun(options, res);
598
624
  await recordTaskRunOutcome({
599
625
  options,
600
- status: result.exitCode === 0 ? "completed" : "failed",
626
+ status: result.streamInterrupted
627
+ ? "running"
628
+ : result.exitCode === 0
629
+ ? "completed"
630
+ : "failed",
601
631
  dialogId: result.dialogId,
602
632
  ...(result.exitCode !== 0 ? { errorCode: "STREAM_FAILED" } : {}),
603
633
  });
@@ -642,9 +672,9 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
642
672
  };
643
673
  await recordTaskRunOutcome({
644
674
  options,
645
- status: "completed",
675
+ status: options.background ? "running" : "completed",
646
676
  dialogId: result.dialogId,
647
- resultSummary: content || undefined,
677
+ ...(!options.background && content ? { resultSummary: content } : {}),
648
678
  });
649
679
  return result;
650
680
  }
@@ -747,9 +777,9 @@ async function runLocalAgentTurnForCli(
747
777
  }
748
778
  await recordTaskRunOutcome({
749
779
  options,
750
- status: "completed",
780
+ status: options.background ? "running" : "completed",
751
781
  dialogId: result.dialogId,
752
- resultSummary: content || undefined,
782
+ ...(!options.background && content ? { resultSummary: content } : {}),
753
783
  toolEvidence: summarizeLocalToolEvidence(toolEvents),
754
784
  });
755
785
  return { exitCode: 0, dialogId: result.dialogId };
@@ -791,15 +821,20 @@ async function readStreamingAgentRun(
791
821
  hasPrintedLabel = true;
792
822
  };
793
823
 
794
- const handlePayload = (payload: any) => {
795
- if (payload?.error || payload?.type === "error") {
796
- throw new Error(String(payload.error || payload.message || "Agent stream failed"));
797
- }
798
- if (payload?.type === "done") {
799
- if (typeof payload.dialogId === "string") dialogId = payload.dialogId;
800
- usage = payload.usage;
801
- return;
802
- }
824
+ const handlePayload = (payload: any) => {
825
+ if (typeof payload?.dialogId === "string" && payload.dialogId.trim()) {
826
+ dialogId = payload.dialogId;
827
+ }
828
+ if (payload?.error || payload?.type === "error") {
829
+ throw new Error(String(payload.error || payload.message || "Agent stream failed"));
830
+ }
831
+ if (payload?.type === "done") {
832
+ usage = payload.usage;
833
+ return;
834
+ }
835
+ if (payload?.type === "dialog" || payload?.type === "status") {
836
+ return;
837
+ }
803
838
 
804
839
  const chunk =
805
840
  payload?.type === "text"
@@ -842,10 +877,18 @@ async function readStreamingAgentRun(
842
877
  .trim();
843
878
  if (raw) handlePayload(JSON.parse(raw));
844
879
  }
845
- } catch (error) {
846
- options.output.write(`\n[nolo] Agent stream failed: ${error instanceof Error ? error.message : String(error)}\n`);
847
- return { exitCode: 1 };
848
- } finally {
880
+ } catch (error) {
881
+ const message = error instanceof Error ? error.message : String(error);
882
+ if (dialogId) {
883
+ options.output.write(
884
+ `\n[nolo] Agent stream transport interrupted after dialog ${dialogId} was created: ${message}\n`
885
+ );
886
+ options.output.write("[nolo] The agent run may still finish on the server; read the dialog before retrying.\n");
887
+ return { exitCode: 0, dialogId, streamInterrupted: true };
888
+ }
889
+ options.output.write(`\n[nolo] Agent stream failed: ${message}\n`);
890
+ return { exitCode: 1 };
891
+ } finally {
849
892
  writer.flushAll();
850
893
  }
851
894
 
@@ -353,6 +353,7 @@ describe("CLI local runtime adapter", () => {
353
353
  tool_choice: "auto",
354
354
  url: "https://api.fireworks.ai/inference/v1/chat/completions",
355
355
  provider: "fireworks",
356
+ agentKey: "agent-user-1-frontend",
356
357
  },
357
358
  });
358
359
  expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
@@ -419,6 +420,7 @@ describe("CLI local runtime adapter", () => {
419
420
  body: {
420
421
  provider: "fireworks",
421
422
  apiSource: "platform",
423
+ agentKey: "agent-user-1-frontend",
422
424
  },
423
425
  });
424
426
  });
@@ -58,6 +58,7 @@ export type CliLocalRuntimeAdapterDeps = {
58
58
  createId?: () => string;
59
59
  fetchImpl?: typeof fetch;
60
60
  cwd?: string;
61
+ useCwdAsTaskWorkspaceBase?: boolean;
61
62
  output?: { write(chunk: string): unknown };
62
63
  prepareTaskWorktree?: typeof prepareTaskWorktree;
63
64
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
@@ -254,7 +255,11 @@ export function createCliLocalRuntimeAdapter(
254
255
  const localToolBudgets = parseLocalToolBudgets(deps.env);
255
256
  const localToolUsage = new Map<string, number>();
256
257
  let activeAgentToolNames: string[] = [];
257
- let workspaceSession: WorkspaceSession = createWorkspaceSession({ cwd: deps.cwd });
258
+ let workspaceSession: WorkspaceSession = createWorkspaceSession(
259
+ deps.useCwdAsTaskWorkspaceBase
260
+ ? { defaultCwd: deps.cwd }
261
+ : { cwd: deps.cwd }
262
+ );
258
263
  let localToolExecutors = buildLocalToolExecutors({
259
264
  workspaceRoot: workspaceSession.workspaceRoot,
260
265
  localToolExecutors: deps.localToolExecutors,
@@ -378,12 +383,20 @@ export function createCliLocalRuntimeAdapter(
378
383
  budgets: localToolBudgets,
379
384
  usage: localToolUsage,
380
385
  });
381
- return executeLocalToolWithPolicy({
386
+ const result = await executeLocalToolWithPolicy({
382
387
  env: deps.env,
383
388
  agentToolNames: activeAgentToolNames,
384
389
  call,
385
390
  executors: localToolExecutors,
386
391
  });
392
+ return {
393
+ ...result,
394
+ metadata: {
395
+ ...(result.metadata ?? {}),
396
+ workspaceRoot: workspaceSession.workspaceRoot,
397
+ workspaceKind: workspaceSession.kind,
398
+ },
399
+ };
387
400
  },
388
401
  };
389
402
  }
@@ -2,7 +2,8 @@
2
2
 
3
3
  import pino from "pino";
4
4
  import { getIsDesktopApp } from "app/utils/env";
5
- import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS } from "../config";
5
+ import { fetchWithTransientReadRetry } from "app/utils/retryFetch";
6
+ import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS, normalizeKnownServerOrigin } from "../config";
6
7
 
7
8
  // RN 下 pino 的 browser 写法可能有兼容性问题
8
9
  // 使用简单的 console 封装作为 fallback
@@ -23,7 +24,7 @@ export const logger = isRN ? {
23
24
  // },
24
25
  });
25
26
  const normalizeServer = (server: string): string =>
26
- server.trim().replace(/\/+$/, "");
27
+ normalizeKnownServerOrigin(server) ?? server.trim().replace(/\/+$/, "");
27
28
  const isNoloClusterServer = (server: string): boolean =>
28
29
  /^https?:\/\/(?:us\.)?nolo\.chat$/i.test(normalizeServer(server));
29
30
 
@@ -202,7 +203,7 @@ export const fetchFromServer = async (
202
203
  signal?.addEventListener("abort", onExternalAbort);
203
204
 
204
205
  try {
205
- const res = await fetch(
206
+ const res = await fetchWithTransientReadRetry(
206
207
  `${server}${buildReadUrl(dbKey)}`,
207
208
  {
208
209
  signal: controller.signal as any,
@@ -8,6 +8,25 @@ export const SERVERS = {
8
8
  } as const;
9
9
  export const NOLO_CLUSTER_SERVERS = Object.values(SERVERS);
10
10
 
11
+ const LEGACY_SERVER_ORIGIN_MAP: Record<string, string> = {
12
+ "https://nolotus.com": SERVERS.MAIN,
13
+ "https://www.nolotus.com": SERVERS.MAIN,
14
+ "https://us.nolotus.com": SERVERS.US,
15
+ "https://www.us.nolotus.com": SERVERS.US,
16
+ };
17
+
18
+ export const normalizeKnownServerOrigin = (server: unknown): string | null => {
19
+ if (typeof server !== "string" || server.trim().length === 0) return null;
20
+ const trimmed = server.trim();
21
+ let origin: string;
22
+ try {
23
+ origin = new URL(trimmed).origin;
24
+ } catch {
25
+ origin = trimmed.replace(/\/+$/, "");
26
+ }
27
+ return LEGACY_SERVER_ORIGIN_MAP[origin.toLowerCase()] ?? origin;
28
+ };
29
+
11
30
  export const API_ENDPOINTS = {
12
31
  DATABASE: `${API_VERSION}/db`,
13
32
  SHARE: `${API_VERSION}/share`,