agentosity 0.1.2 → 0.1.4

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/probes.js +67 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentosity",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Agentosity — AI-native is a number now. Automatic attendance for your AI agents (stdio MCP lifecycle + activity probes) + human clock-out CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/probes.js CHANGED
@@ -1,7 +1,8 @@
1
- import { readdirSync, statSync, openSync, readSync, closeSync, fstatSync } from "node:fs";
1
+ import { readdirSync, statSync, openSync, readSync, closeSync, fstatSync, existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
+ import { createHash } from "node:crypto";
5
6
 
6
7
  /**
7
8
  * 活跃度探针:回答"此刻 Agent 是否在干活"。
@@ -18,6 +19,7 @@ export function detectHarness(clientInfo) {
18
19
  if (n.includes("gemini")) return "gemini-cli";
19
20
  if (n.includes("cursor")) return "cursor";
20
21
  if (n.includes("cline")) return "cline";
22
+ if (n.includes("opencode")) return "opencode";
21
23
  return n || "unknown";
22
24
  }
23
25
 
@@ -38,30 +40,66 @@ function codexSessionDir() {
38
40
  );
39
41
  }
40
42
 
43
+ /** Gemini CLI:~/.gemini/tmp/<sha256(cwd)>/ 下的日志文件 */
44
+ function geminiSessionDir() {
45
+ const hash = createHash("sha256").update(process.cwd()).digest("hex");
46
+ return join(homedir(), ".gemini", "tmp", hash);
47
+ }
48
+
49
+ /**
50
+ * 各 harness 的探针配置:
51
+ * - dir:目录内找"我们这个会话"的文件(新建或最近在写的)
52
+ * - file:固定文件,直接看 mtime(全局共享,精度低一档但胜过没有)
53
+ * - tail:文件是 jsonl 时才解析尾巴的在途工具调用
54
+ */
55
+ const PROBE_TARGETS = {
56
+ "claude-code": { dir: claudeSessionDir, ext: ".jsonl", tail: true },
57
+ codex: { dir: codexSessionDir, ext: ".jsonl", tail: true },
58
+ "gemini-cli": { dir: geminiSessionDir, ext: "", tail: false },
59
+ opencode: {
60
+ file: () => join(homedir(), ".local", "share", "opencode", "opencode.db-wal"),
61
+ tail: false,
62
+ },
63
+ };
64
+
41
65
  export function createProbe(harness, processStartMs) {
66
+ const target = PROBE_TARGETS[harness];
42
67
  const state = {
43
68
  kind: "none",
44
69
  file: null,
70
+ tail: target?.tail ?? false,
45
71
  lastMtimeMs: 0,
46
72
  lastWriteAt: 0,
73
+ baselineChildren: null, // harness 的常驻子进程基线(其他 MCP server 等),它们在 ≠ 在干活
47
74
  };
48
75
 
49
- const dirOf = { "claude-code": claudeSessionDir, codex: codexSessionDir }[harness];
50
-
51
76
  function bindFile() {
52
- if (state.file || !dirOf) return;
77
+ if (state.file || !target) return;
53
78
  try {
54
- const dir = dirOf();
79
+ if (target.file) {
80
+ const p = target.file();
81
+ if (!existsSync(p)) return;
82
+ const st = statSync(p);
83
+ state.file = p;
84
+ state.kind = "file-mtime";
85
+ state.lastMtimeMs = st.mtimeMs;
86
+ state.lastWriteAt = Date.now();
87
+ return;
88
+ }
89
+ const dir = target.dir();
55
90
  const candidates = readdirSync(dir)
56
- .filter((f) => f.endsWith(".jsonl"))
91
+ .filter((f) => (target.ext ? f.endsWith(target.ext) : true))
57
92
  .map((f) => {
58
93
  const p = join(dir, f);
59
94
  const st = statSync(p);
95
+ if (!st.isFile()) return null;
60
96
  return { p, birth: st.birthtimeMs || st.ctimeMs, mtime: st.mtimeMs };
61
97
  })
62
- // 会话文件在首条消息时才创建,允许比进程启动早 2 分钟(时钟偏差)之后的任何时间
63
- .filter((c) => c.birth >= processStartMs - 120_000)
64
- .sort((a, b) => b.birth - a.birth);
98
+ .filter(Boolean)
99
+ // 会话文件可能是新建的(新会话),也可能是续用的老文件(resume):
100
+ // 新建:创建时间在进程启动前后;续用:进程启动后仍在被写入
101
+ .filter((c) => c.birth >= processStartMs - 120_000 || c.mtime >= processStartMs - 5_000)
102
+ .sort((a, b) => b.mtime - a.mtime);
65
103
  if (candidates.length > 0) {
66
104
  state.file = candidates[0].p;
67
105
  state.kind = "file-mtime";
@@ -90,9 +128,9 @@ export function createProbe(harness, processStartMs) {
90
128
  }
91
129
  }
92
130
 
93
- /** 信号 2:尾巴状态 — 最后一个事件是发起工具调用且尚无结果 → 工具在途 */
131
+ /** 信号 2:尾巴状态 — 最后一个事件是发起工具调用且尚无结果 → 工具在途(仅 jsonl) */
94
132
  function toolInFlight() {
95
- if (!state.file) return false;
133
+ if (!state.file || !state.tail) return false;
96
134
  try {
97
135
  const fd = openSync(state.file, "r");
98
136
  const size = fstatSync(fd).size;
@@ -112,18 +150,30 @@ export function createProbe(harness, processStartMs) {
112
150
  }
113
151
  }
114
152
 
115
- /** 信号 3:harness(父进程)有除本进程外的子进程在跑(长工具调用) */
153
+ /** 信号 3:harness(父进程)出现了基线之外的新子进程(= 工具调用在跑)
154
+ * 基线 = 探针启动时已存在的子进程(自己和其他常驻 MCP server),它们一直在,不代表在干活。 */
116
155
  function childActive() {
117
156
  try {
118
157
  const out = execFileSync("pgrep", ["-P", String(process.ppid)], {
119
158
  encoding: "utf8",
120
159
  timeout: 3000,
121
160
  });
122
- const others = out
123
- .split("\n")
124
- .map((s) => parseInt(s, 10))
125
- .filter((pid) => pid && pid !== process.pid);
126
- return others.length > 0;
161
+ const current = new Set(
162
+ out
163
+ .split("\n")
164
+ .map((s) => parseInt(s, 10))
165
+ .filter((pid) => pid && pid !== process.pid)
166
+ );
167
+ if (state.baselineChildren === null) {
168
+ state.baselineChildren = current;
169
+ return false;
170
+ }
171
+ // 死掉的移出基线;有基线外的新面孔才算干活
172
+ state.baselineChildren = new Set([...state.baselineChildren].filter((p) => current.has(p)));
173
+ for (const p of current) {
174
+ if (!state.baselineChildren.has(p)) return true;
175
+ }
176
+ return false;
127
177
  } catch {
128
178
  return false; // pgrep 无匹配时 exit 1,也走这里 → 视为无子进程
129
179
  }