@fancyboi999/open-tag-daemon 0.6.0 → 0.7.0

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.
@@ -3079,8 +3079,9 @@ function createLogger(component) {
3079
3079
  }
3080
3080
  const extra = fields && Object.keys(fields).length ? " " + safeJson(fields) : "";
3081
3081
  const line = `${rec.t} ${level.toUpperCase().padEnd(5)} [${component}] ${msg}${extra}`;
3082
+ const stream = consoleStream(component, level);
3082
3083
  try {
3083
- process.stderr.write(line + "\n");
3084
+ stream.write(line + "\n");
3084
3085
  } catch {
3085
3086
  }
3086
3087
  }
@@ -3092,6 +3093,10 @@ function createLogger(component) {
3092
3093
  child: (sub) => createLogger(`${component}:${sub}`)
3093
3094
  };
3094
3095
  }
3096
+ function consoleStream(component, level) {
3097
+ if (level === "error" || component === "cli" || component.startsWith("cli:")) return process.stderr;
3098
+ return process.stdout;
3099
+ }
3095
3100
  function safeJson(o) {
3096
3101
  try {
3097
3102
  return JSON.stringify(o, (_k, v) => typeof v === "string" && v.length > 300 ? v.slice(0, 300) + "\u2026" : v);
@@ -3151,7 +3156,7 @@ message.command("check").description("non-blocking check for new messages").acti
3151
3156
  for (const m of d.messages) console.log(m.text);
3152
3157
  console.log("No more new messages.");
3153
3158
  });
3154
- message.command("send").description("send a message (body read from stdin); if new messages arrived since last read the message is freshness-held as a draft \u2014 revise it or use --send-draft to submit as-is").requiredOption("--target <target>", "#channel / dm:@name / #channel:shortid").option("--attach <ids>", "attachment ids, comma-separated").option("--send-draft", "submit the held draft as-is, bypassing freshness check").action(async (opts) => {
3159
+ message.command("send").description("send a message (body read from stdin); if new messages arrived since last read the message is freshness-held as a draft \u2014 revise it or use --send-draft to submit as-is").requiredOption("--target <target>", "#channel / dm:@name / #channel:shortid / thread:shortid").option("--attach <ids>", "attachment ids, comma-separated").option("--send-draft", "submit the held draft as-is, bypassing freshness check").action(async (opts) => {
3155
3160
  const sendDraft = !!opts.sendDraft;
3156
3161
  const content = sendDraft ? "" : (await readStdin()).trim();
3157
3162
  const attachmentIds = opts.attach ? String(opts.attach).split(",").map((s) => s.trim()).filter(Boolean) : [];
@@ -3227,6 +3232,16 @@ task.command("update").description("update task status (--message-id, or --chann
3227
3232
  await api("POST", "/agent-api/task/update", body);
3228
3233
  console.log(`Updated -> ${opts.status}`);
3229
3234
  });
3235
+ task.command("assign").description("hand off a task to another agent (--message-id, or --channel #ch --number N)").option("--message-id <id>").option("--channel <ch>", "#name / dm:@name (used with --number)").option("--number <n>", "task number #N").requiredOption("--to <agent>", "@agent handle").action(async (opts) => {
3236
+ const body = { to: opts.to };
3237
+ if (opts.number != null) {
3238
+ body.channel = opts.channel;
3239
+ body.number = Number(opts.number);
3240
+ } else body.messageId = opts.messageId;
3241
+ const d = await api("POST", "/agent-api/task/assign", body);
3242
+ console.log(`Assigned task #${d.number ?? "?"} -> @${d.to}`);
3243
+ if (d.followUp) console.log(d.followUp);
3244
+ });
3230
3245
  var taskCreate = async (opts) => {
3231
3246
  const d = await api("POST", "/agent-api/task/new", { target: opts.channel, title: opts.title });
3232
3247
  for (const t of d.tasks ?? []) console.log(`Created task #${t.number ?? "-"} ${String(t.id).slice(0, 8)}: ${t.content}`);
@@ -3251,7 +3266,7 @@ thread.command("reply").description("start or reply to a thread under a message
3251
3266
  const d = await api("POST", "/agent-api/thread/reply", { parent: opts.parent, channel: opts.channel, content });
3252
3267
  console.log(`Replied in thread (thread ${String(d.threadChannelId).slice(0, 8)}, msg ${String(d.id).slice(0, 8)})`);
3253
3268
  });
3254
- thread.command("unfollow").description("stop receiving deliveries from a thread").requiredOption("--target <thread>", "#channel:shortid").action(async (opts) => {
3269
+ thread.command("unfollow").description("stop receiving deliveries from a thread").requiredOption("--target <thread>", "#channel:shortid or thread:shortid").action(async (opts) => {
3255
3270
  await api("POST", "/agent-api/thread/unfollow", { target: opts.target });
3256
3271
  console.log(`Unfollowed ${opts.target}`);
3257
3272
  });
package/dist/cli.mjs CHANGED
@@ -3770,8 +3770,9 @@ function createLogger(component) {
3770
3770
  }
3771
3771
  const extra = fields && Object.keys(fields).length ? " " + safeJson(fields) : "";
3772
3772
  const line = `${rec.t} ${level.toUpperCase().padEnd(5)} [${component}] ${msg}${extra}`;
3773
+ const stream = consoleStream(component, level);
3773
3774
  try {
3774
- process.stderr.write(line + "\n");
3775
+ stream.write(line + "\n");
3775
3776
  } catch {
3776
3777
  }
3777
3778
  }
@@ -3783,6 +3784,10 @@ function createLogger(component) {
3783
3784
  child: (sub) => createLogger(`${component}:${sub}`)
3784
3785
  };
3785
3786
  }
3787
+ function consoleStream(component, level) {
3788
+ if (level === "error" || component === "cli" || component.startsWith("cli:")) return process.stderr;
3789
+ return process.stdout;
3790
+ }
3786
3791
  function safeJson(o) {
3787
3792
  try {
3788
3793
  return JSON.stringify(o, (_k, v) => typeof v === "string" && v.length > 300 ? v.slice(0, 300) + "\u2026" : v);
@@ -3797,6 +3802,7 @@ var MACHINE_REJECTED_CODE = 4001;
3797
3802
  // src/daemon/connection.ts
3798
3803
  var INITIAL_BACKOFF_MS = 1e3;
3799
3804
  var MAX_BACKOFF_MS = 3e4;
3805
+ var SERVER_STALE_MS = Number(process.env.OPEN_TAG_DAEMON_SERVER_STALE_MS ?? 9e4);
3800
3806
  var Connection = class {
3801
3807
  constructor(url, key, onMsg, onOpen, mkWs = (u) => new wrapper_default(u)) {
3802
3808
  this.url = url;
@@ -3813,9 +3819,11 @@ var Connection = class {
3813
3819
  ws = null;
3814
3820
  delay = INITIAL_BACKOFF_MS;
3815
3821
  timer = null;
3822
+ watchdog = null;
3816
3823
  should = true;
3817
3824
  accepted = false;
3818
3825
  // per-attempt: flips true once the server sends any frame (proof it accepted us, not rejected)
3826
+ lastServerFrameAt = 0;
3819
3827
  log = createLogger("daemon:conn");
3820
3828
  connect() {
3821
3829
  this.should = true;
@@ -3827,13 +3835,22 @@ var Connection = class {
3827
3835
  close() {
3828
3836
  this.should = false;
3829
3837
  if (this.timer) clearTimeout(this.timer);
3838
+ if (this.watchdog) {
3839
+ clearTimeout(this.watchdog);
3840
+ this.watchdog = null;
3841
+ }
3830
3842
  this.ws?.close();
3831
3843
  }
3832
3844
  doConnect() {
3833
3845
  if (!this.should) return;
3834
3846
  const wsUrl = this.url.replace(/^http/, "ws") + `/daemon/connect?key=${encodeURIComponent(this.key)}`;
3835
3847
  this.log.info("connecting", { url: this.url });
3848
+ if (this.watchdog) {
3849
+ clearTimeout(this.watchdog);
3850
+ this.watchdog = null;
3851
+ }
3836
3852
  this.accepted = false;
3853
+ this.lastServerFrameAt = 0;
3837
3854
  this.ws = this.mkWs(wsUrl);
3838
3855
  this.ws.on("open", () => {
3839
3856
  this.log.info("connected");
@@ -3844,6 +3861,8 @@ var Connection = class {
3844
3861
  this.accepted = true;
3845
3862
  this.delay = INITIAL_BACKOFF_MS;
3846
3863
  }
3864
+ this.lastServerFrameAt = Date.now();
3865
+ this.armWatchdog();
3847
3866
  let m;
3848
3867
  try {
3849
3868
  m = JSON.parse(d.toString());
@@ -3853,6 +3872,10 @@ var Connection = class {
3853
3872
  this.onMsg(m);
3854
3873
  });
3855
3874
  this.ws.on("close", (code, reason) => {
3875
+ if (this.watchdog) {
3876
+ clearTimeout(this.watchdog);
3877
+ this.watchdog = null;
3878
+ }
3856
3879
  if (code === MACHINE_REJECTED_CODE) {
3857
3880
  this.delay = MAX_BACKOFF_MS;
3858
3881
  this.log.error(
@@ -3866,6 +3889,18 @@ var Connection = class {
3866
3889
  });
3867
3890
  this.ws.on("error", (e) => this.log.error("ws error", { detail: String(e?.message ?? e) }));
3868
3891
  }
3892
+ armWatchdog() {
3893
+ if (this.watchdog) clearTimeout(this.watchdog);
3894
+ this.watchdog = setTimeout(() => {
3895
+ if (!this.accepted || !this.lastServerFrameAt || this.ws?.readyState !== wrapper_default.OPEN) return;
3896
+ this.log.warn("server heartbeat stale; closing socket to reconnect", { staleMs: Date.now() - this.lastServerFrameAt });
3897
+ try {
3898
+ this.ws.close();
3899
+ } catch {
3900
+ }
3901
+ }, SERVER_STALE_MS + 1);
3902
+ this.watchdog.unref?.();
3903
+ }
3869
3904
  scheduleReconnect() {
3870
3905
  if (!this.should || this.timer) return;
3871
3906
  this.log.info("reconnecting", { ms: this.delay });
@@ -3902,8 +3937,8 @@ A local \`open-tag\` command is on your PATH. Use ONLY it to communicate, via yo
3902
3937
  - \`open-tag message read --channel <t> [--limit N]\` \u2014 read history.
3903
3938
  - \`open-tag server info\` \u2014 list channels / agents / humans.
3904
3939
  - \`open-tag channel join --target "#name"\` \u2014 join a public channel.
3905
- - \`open-tag task list --channel <t>\` \xB7 \`open-tag task claim --message-id <id>\` \xB7 \`open-tag task update --message-id <id> --status <todo|in_progress|in_review|done>\` \xB7 \`open-tag task create --channel <t> --title <t>\`(delegate a task)
3906
- - **Threads (no dedicated thread command \u2014 use message send/read with a thread-suffix target)**: reply to / open a thread = \`open-tag message send --target "#channel:shortid"\` (suffix shortid = the 8-char short id from the \`msg=\` field in the message header; if it does not exist, a thread is created automatically); read a thread = \`open-tag message read --channel "#channel:shortid"\`; stop receiving deliveries for a thread = \`open-tag thread unfollow --target "#channel:shortid"\` (only when work in that thread is clearly done or irrelevant). Threads cannot be nested.
3940
+ - \`open-tag task list --channel <t>\` \xB7 \`open-tag task claim --message-id <id>\` \xB7 \`open-tag task assign --message-id <id> --to @agent\`(handoff to another agent) \xB7 \`open-tag task update --message-id <id> --status <todo|in_progress|in_review|done>\` \xB7 \`open-tag task create --channel <t> --title <t>\`(delegate a task)
3941
+ - **Threads (no dedicated thread command \u2014 use a thread target)**: reply to / open a thread = \`open-tag message send --target "#channel:shortid"\` or the stable \`thread:shortid\` form (where \`shortid\` is the 8-char parent message id from the message header; if the thread does not exist yet, the target creates it automatically when the parent channel is accessible); read a thread = \`open-tag message read --channel "thread:shortid"\`; stop receiving deliveries for a thread = \`open-tag thread unfollow --target "thread:shortid"\` (or the older \`#channel:shortid\` form) when work there is clearly done or irrelevant. Threads cannot be nested.
3907
3942
  - \`open-tag message react --message-id <id> --emoji <e> [--remove]\`(emoji reaction) \xB7 \`open-tag message search --query <q>\`(search channels you are in)
3908
3943
  - \`open-tag attachment upload --file <path> --channel <t>\`(upload a file, returns an id; then use \`message send --attach <id>\`) \xB7 \`open-tag attachment view --id <id>\`(downloads the attachment to the local \`attachments/\` directory and prints its local path for inspection \u2014 this command only handles the download and path; how you open it is up to your local tools)
3909
3944
  - \`open-tag message resolve --id <id>\`(verify that a cited message id is real \u2014 always resolve before referencing, never invent ids from memory) \xB7 \`open-tag channel members --channel <t>\` \xB7 \`open-tag channel leave --target "#name"\` \xB7 \`open-tag task unclaim --message-id <id>\`
@@ -3911,7 +3946,7 @@ A local \`open-tag\` command is on your PATH. Use ONLY it to communicate, via yo
3911
3946
  - \`open-tag reminder schedule --content <t> --in <seconds> [--anchor <msgId>] [--recurring <seconds>]\`(schedule a future wakeup for yourself \u2014 at the scheduled time the system will @-mention you to wake you up) \xB7 \`open-tag reminder list/cancel/snooze\`. For anything that depends on a future state, use a reminder instead of busy-waiting.
3912
3947
  - \`open-tag action prepare --target <t>\` \u2014 prepare an action card for a human to commit (B-mode quick-commit). You do NOT have permission to create channels/agents yourself; instead pipe the action JSON on STDIN and post a card the human clicks to execute under their own identity. Variants: \`channel:create\` (\`{"type":"channel:create","name":"x","description":"...","visibility":"public"}\`), \`agent:create\` (\`{"type":"agent:create","name":"y","description":"..."}\`). Use when a human asks you to set up a channel/agent \u2014 propose it as a card, don't ask them to do it manually.
3913
3948
 
3914
- Targets: \`#channel\`, \`dm:@name\`, thread \`#channel:shortid\`. Send the body via stdin heredoc:
3949
+ Targets: \`#channel\`, \`dm:@name\`, thread \`#channel:shortid\` or \`thread:shortid\`. Prefer \`thread:shortid\` when reusing a thread target across different agents, private channels, or DMs because it is stable across actor viewpoints. Send the body via stdin heredoc:
3915
3950
  \`\`\`bash
3916
3951
  open-tag message send --target "#all" <<'MSG'
3917
3952
  Your reply. Quotes, $vars, \`backticks\`, code blocks are all safe here.
@@ -3923,11 +3958,11 @@ FRESHNESS HOLD (collaboration safety): if new messages arrived in that target si
3923
3958
 
3924
3959
  ## Received message format
3925
3960
  \`[target=<id> msg=<shortid> time=<iso> type=human|agent|system] @sender: content\`
3926
- Reuse the \`target=\` value when replying so it lands in the right channel/DM/thread. @mention people by their @handle. \`msg=\` is the 8-char short id \u2014 use it as a thread suffix (\`#channel:shortid\`) to start/reply in a thread, and pass it to \`open-tag message resolve\` to verify a cited id is real. \`type=system\` messages announce state changes (task events, reminders) \u2014 don't reply unless they clearly ask you to act.
3961
+ Reuse the \`target=\` value when replying so it lands in the right channel/DM/thread. @mention people by their @handle. \`msg=\` is the 8-char short id \u2014 use it as a thread suffix (\`#channel:shortid\`) or as the stable form \`thread:shortid\` to start/reply in a thread, and pass it to \`open-tag message resolve\` to verify a cited id is real. \`type=system\` messages announce state changes (task events, reminders) \u2014 don't reply unless they clearly ask you to act.
3927
3962
 
3928
3963
  ### Formatting \u2014 so refs/links render
3929
3964
  open-tag auto-renders these **bare-text** tokens into clickable refs; write them as plain words, NOT wrapped in backticks (code spans are literal, won't render):
3930
- - \`@handle\` \u2192 user/agent \xB7 \`#channel\` \u2192 channel \xB7 \`#channel:shortid\` \u2192 thread \xB7 \`task #N\` \u2192 task (write "task #N", not bare "#N").
3965
+ - \`@handle\` \u2192 user/agent \xB7 \`#channel\` \u2192 channel \xB7 \`#channel:shortid\` or \`thread:shortid\` \u2192 thread \xB7 \`task #N\` \u2192 task (write "task #N", not bare "#N").
3931
3966
  - **URL next to CJK/non-ASCII punctuation**: wrap it in \`<url>\` or \`[text](url)\`, else the punctuation gets swallowed into the link. Wrong: \`env:http://x:3000,see\` \u2192 Right: \`env:<http://x:3000>,see\`.
3932
3967
 
3933
3968
  ### Citing prior discussion
@@ -5956,7 +5991,7 @@ conn = new Connection(serverUrl, apiKey, (msg) => {
5956
5991
  runningAgents: mgr.running(),
5957
5992
  hostname: os4.hostname(),
5958
5993
  os: `${os4.platform()} ${os4.arch()}`,
5959
- daemonVersion: "0.6.0",
5994
+ daemonVersion: "0.7.0",
5960
5995
  machineId: readMachineId()
5961
5996
  // Stable identity: empty on first connection; server sends it back via ready:ack for persistence.
5962
5997
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fancyboi999/open-tag-daemon",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "open-tag compute-plane daemon — connect any machine to an open-tag server so its agents run there. No repo clone needed.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",