@tamsensedev/runner 0.19.0 → 0.20.1

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/cli.js CHANGED
@@ -906,6 +906,7 @@ function helperStopRequest(stop) {
906
906
  var DEFAULT_MODEL = "default";
907
907
  var QUIET_MS = 1e3;
908
908
  var IDLE_LIMIT_MS = 3e4;
909
+ var BACKGROUND_LIMIT_MS = 6e5;
909
910
  function userMessage(text2) {
910
911
  return { type: "user", message: { role: "user", content: text2 }, parent_tool_use_id: null };
911
912
  }
@@ -926,6 +927,12 @@ function switchLine(change) {
926
927
  function isActivity(message) {
927
928
  return message.type === "assistant" || message.type === "user" || message.type === "system" && message.subtype === "init";
928
929
  }
930
+ function backgroundOf(message) {
931
+ if (message.type !== "system" || message.subtype !== "background_tasks_changed") {
932
+ return null;
933
+ }
934
+ return message.tasks.filter((task) => !task.ambient).length;
935
+ }
929
936
  function inputChannel(options = {}) {
930
937
  const queue = [];
931
938
  const seen = /* @__PURE__ */ new Set();
@@ -941,6 +948,7 @@ function inputChannel(options = {}) {
941
948
  let quiet = false;
942
949
  let idleEpoch = 0;
943
950
  let inFlight = 0;
951
+ let background = 0;
944
952
  let statesSeen = false;
945
953
  let wake = null;
946
954
  let timers = [];
@@ -966,7 +974,7 @@ function inputChannel(options = {}) {
966
974
  wakeUp();
967
975
  }
968
976
  function maybeClose() {
969
- if (started && idle && drained && quiet && queue.length === 0 && inFlight === 0) {
977
+ if (started && idle && drained && quiet && background === 0 && queue.length === 0 && inFlight === 0) {
970
978
  close();
971
979
  }
972
980
  }
@@ -974,23 +982,34 @@ function inputChannel(options = {}) {
974
982
  idle = false;
975
983
  clearTimers();
976
984
  }
977
- function turnEnded() {
978
- if (closed || idle) {
979
- return;
980
- }
981
- idle = true;
982
- idleEpoch += 1;
983
- drained = !options.onIdle;
985
+ function armTimers() {
986
+ clearTimers();
984
987
  quiet = false;
985
- timers = [
988
+ timers = background > 0 ? [setTimeout(close, options.backgroundLimitMs ?? BACKGROUND_LIMIT_MS)] : [
986
989
  setTimeout(() => {
987
990
  quiet = true;
988
991
  maybeClose();
989
992
  }, options.quietMs ?? QUIET_MS),
990
993
  setTimeout(close, options.idleLimitMs ?? IDLE_LIMIT_MS)
991
994
  ];
995
+ }
996
+ function turnEnded() {
997
+ if (closed || idle) {
998
+ return;
999
+ }
1000
+ idle = true;
1001
+ idleEpoch += 1;
1002
+ drained = !options.onIdle;
1003
+ armTimers();
992
1004
  options.onIdle?.();
993
1005
  }
1006
+ function backgroundChanged(count) {
1007
+ const was = background;
1008
+ background = count;
1009
+ if (idle && !closed && was > 0 !== count > 0) {
1010
+ armTimers();
1011
+ }
1012
+ }
994
1013
  async function* stream(own) {
995
1014
  for (; ; ) {
996
1015
  if (closed || own !== generation) {
@@ -1032,6 +1051,11 @@ function inputChannel(options = {}) {
1032
1051
  },
1033
1052
  // Если Claude Code сообщает состояние сессии, верим только ему: idle приходит после фоновых задач
1034
1053
  observe(message) {
1054
+ const tasks = backgroundOf(message);
1055
+ if (tasks !== null) {
1056
+ backgroundChanged(tasks);
1057
+ return;
1058
+ }
1035
1059
  if (message.type === "system" && message.subtype === "session_state_changed") {
1036
1060
  statesSeen = true;
1037
1061
  if (message.state === "idle") {
@@ -3918,7 +3942,7 @@ function describeDevice(identity) {
3918
3942
  }
3919
3943
 
3920
3944
  // src/sense.ts
3921
- import { mkdtemp, rm as rm5 } from "fs/promises";
3945
+ import { mkdtemp, rm as rm5, stat as stat2 } from "fs/promises";
3922
3946
  import { tmpdir } from "os";
3923
3947
  import { dirname as dirname4, join as join9 } from "path";
3924
3948
  import process5 from "process";
@@ -3971,7 +3995,8 @@ async function sleep(ms, signal) {
3971
3995
 
3972
3996
  // src/sense.ts
3973
3997
  var SERVER_NAME3 = "tamsense";
3974
- var MAX_SENSE_TURNS = 3;
3998
+ var WEB_TOOL = "WebFetch";
3999
+ var MAX_SENSE_TURNS = 6;
3975
4000
  var HEARTBEAT_MS = 5e3;
3976
4001
  var STOPPED_HERE = "stopped_here";
3977
4002
  var CLOSED_ON_SITE = "closed_on_site";
@@ -3989,12 +4014,13 @@ function claudeSenseRunner(startQuery) {
3989
4014
  options: {
3990
4015
  cwd: request.cwd,
3991
4016
  systemPrompt: request.system,
3992
- tools: [],
4017
+ tools: [WEB_TOOL],
3993
4018
  settingSources: [],
3994
4019
  persistSession: false,
4020
+ ...request.resume === null ? {} : { resume: request.resume, forkSession: true },
3995
4021
  strictMcpConfig: true,
3996
4022
  mcpServers: tools.length > 0 ? { [SERVER_NAME3]: createSdkMcpServer3({ name: SERVER_NAME3, version: "1.0.0", tools }) } : {},
3997
- allowedTools: tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`),
4023
+ allowedTools: [WEB_TOOL, ...tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`)],
3998
4024
  permissionMode: "auto",
3999
4025
  canUseTool: denyWhenAsked,
4000
4026
  maxTurns: MAX_SENSE_TURNS,
@@ -4027,8 +4053,17 @@ function senseHeartbeat(api, job, abort) {
4027
4053
  }
4028
4054
  };
4029
4055
  }
4056
+ async function authorSession(job, dataFolder) {
4057
+ if (!job.author) {
4058
+ return null;
4059
+ }
4060
+ const folder = taskFolder(dataFolder, { taskId: job.taskId, stage: { columnId: job.author.columnId } });
4061
+ const found = await stat2(folder).catch(() => null);
4062
+ return found?.isDirectory() ? { folder, sessionId: job.author.sessionId } : null;
4063
+ }
4030
4064
  async function runSenseJob(job, runSense, tools, stop) {
4031
- const cwd = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
4065
+ const scratch = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
4066
+ const author = await authorSession(job, tools.dataFolder);
4032
4067
  const run2 = new AbortController();
4033
4068
  const stopRun = () => run2.abort(STOPPED_HERE);
4034
4069
  if (stop.aborted) {
@@ -4038,17 +4073,24 @@ async function runSenseJob(job, runSense, tools, stop) {
4038
4073
  const deadline = setTimeout(() => run2.abort(TOO_LONG), job.deadlineMs);
4039
4074
  const beat = senseHeartbeat(tools.api, job, () => run2.abort(CLOSED_ON_SITE));
4040
4075
  const beating = setInterval(() => void beat(), HEARTBEAT_MS);
4041
- let body;
4076
+ let body = FAILED;
4042
4077
  try {
4043
4078
  const env = withNodeOnPath(process5.env, dirname4(process5.execPath));
4044
- body = await runSense({ system: job.system, prompt: job.prompt, schema: job.schema, model: job.model, cwd, env, signal: run2.signal, sources: sourceTools(job.tools, tools.api, sensePath(job.senseId, "sources")) });
4079
+ const sources = sourceTools(job.tools, tools.api, sensePath(job.senseId, "sources"));
4080
+ const ask = (cwd, resume) => runSense({ system: job.system, prompt: job.prompt, schema: job.schema, model: job.model, cwd, resume, env, signal: run2.signal, sources }).catch(() => FAILED);
4081
+ if (author) {
4082
+ body = await ask(author.folder, author.sessionId);
4083
+ }
4084
+ if (body.outcome === "failed" && !run2.signal.aborted) {
4085
+ body = await ask(scratch, null);
4086
+ }
4045
4087
  } catch {
4046
4088
  body = FAILED;
4047
4089
  } finally {
4048
4090
  clearTimeout(deadline);
4049
4091
  clearInterval(beating);
4050
4092
  stop.removeEventListener("abort", stopRun);
4051
- await rm5(cwd, { recursive: true, force: true });
4093
+ await rm5(scratch, { recursive: true, force: true });
4052
4094
  }
4053
4095
  if (run2.signal.reason === CLOSED_ON_SITE) {
4054
4096
  return;
@@ -4218,7 +4260,7 @@ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
4218
4260
  // src/recordings.ts
4219
4261
  import { randomUUID } from "crypto";
4220
4262
  import { createReadStream } from "fs";
4221
- import { readdir as readdir2, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
4263
+ import { readdir as readdir2, readFile as readFile4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
4222
4264
  import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join11 } from "path";
4223
4265
  import { createInterface as createInterface2 } from "readline";
4224
4266
  import { Readable } from "stream";
@@ -4325,7 +4367,7 @@ async function remember(dir, file) {
4325
4367
  await writeFile4(join11(dir, LEDGER_FILE), JSON.stringify([...done2]));
4326
4368
  }
4327
4369
  async function nonEmpty(file) {
4328
- return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
4370
+ return file !== null && ((await stat3(file).catch(() => null))?.size ?? 0) > 0;
4329
4371
  }
4330
4372
  async function pendingRecordings(dir) {
4331
4373
  const done2 = await ledger(dir);
@@ -4360,7 +4402,7 @@ async function uploadReplay(upload, file, label) {
4360
4402
  }
4361
4403
  async function uploadVideo(upload, file, label, meta) {
4362
4404
  const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
4363
- const info = await stat2(file);
4405
+ const info = await stat3(file);
4364
4406
  if (info.size > MAX_VIDEO_BYTES) {
4365
4407
  throw new Error(`The video is larger than ${MAX_VIDEO_BYTES / 1024 / 1024} MB. Record a shorter one.`);
4366
4408
  }
@@ -4449,7 +4491,7 @@ async function uploadPendingRecordings(api, runId, dir) {
4449
4491
  }
4450
4492
 
4451
4493
  // src/screenshot.ts
4452
- import { readFile as readFile5, stat as stat3 } from "fs/promises";
4494
+ import { readFile as readFile5, stat as stat4 } from "fs/promises";
4453
4495
  import { basename as basename5, extname as extname3, isAbsolute as isAbsolute4 } from "path";
4454
4496
  var TYPES_BY_EXTENSION2 = {
4455
4497
  ".png": "image/png",
@@ -4471,7 +4513,7 @@ async function readScreenshot(request) {
4471
4513
  } catch (error) {
4472
4514
  return error instanceof Error ? error.message : String(error);
4473
4515
  }
4474
- const info = await stat3(request.path).catch(() => null);
4516
+ const info = await stat4(request.path).catch(() => null);
4475
4517
  if (!info?.isFile()) {
4476
4518
  return `There is no file at ${request.path}.`;
4477
4519
  }