thincoder 0.12.27 → 0.12.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.27",
3
+ "version": "0.12.29",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -80,12 +80,20 @@ export async function prepareRun(agent, input, callbacks, {
80
80
  const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
81
81
  const wasRestored = agent._sessionStart != null
82
82
  agent._sessionStart ??= new Date().toISOString()
83
- if (tree) {
84
- agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}. Working directory snapshot:\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
85
- } else {
86
- agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.]`, transient: true })
83
+ // Inject OS/cwd + cwd-tree only ONCE per process (restored sessionStart keeps the
84
+ // content byte-identical across restarts 2026-08-16 cache audit). Without the guard
85
+ // every run re-pushed the same reminder (history bloat, wasted tokens).
86
+ if (!agent._osReminderInjected) {
87
+ agent._osReminderInjected = true
88
+ if (tree) {
89
+ agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}. Working directory snapshot:\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
90
+ } else {
91
+ agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.]`, transient: true })
92
+ }
93
+
87
94
  }
88
- if (wasRestored) {
95
+ if (wasRestored && !agent._restartReminderInjected) {
96
+ agent._restartReminderInjected = true
89
97
  agent.history.push({ role: "user", content: `[System reminder: process restarted at ${new Date().toISOString()}.]`, transient: true })
90
98
  }
91
99
  if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
@@ -124,13 +132,6 @@ export async function prepareRun(agent, input, callbacks, {
124
132
  })
125
133
  }
126
134
  }
127
- // Time grounding for EVERY agent depth (subagents are short-lived; they also need
128
- // to know "now"). Transient — dropped on persist, fresh at every run start.
129
- agent.history.push({
130
- role: "user",
131
- content: `[System reminder: current time is ${timeNowLocal()} (local; timezone ${Intl.DateTimeFormat().resolvedOptions().timeZone || "local"}).]`,
132
- transient: true,
133
- })
134
135
  if (depth === 0) {
135
136
  // Checklist injection: inject pending + in_progress items from .thincoder/checklist.md
136
137
  try {
@@ -147,6 +148,15 @@ export async function prepareRun(agent, input, callbacks, {
147
148
  }
148
149
  pushReal(agent, { role: "user", content: input })
149
150
  }
151
+ // Time grounding for EVERY agent depth AND every resume, pushed LAST (after the user
152
+ // input): transient, dropped on persist, fresh at every run start — including resumes
153
+ // (an interrupt-continuation must know NOW, not the pre-interrupt time; 2026-08-16).
154
+ // Tail position keeps the second-precision content out of any prefix — caches stay hit.
155
+ agent.history.push({
156
+ role: "user",
157
+ content: `[System reminder: current time is ${timeNowLocal()} (local; timezone ${Intl.DateTimeFormat().resolvedOptions().timeZone || "local"}).`,
158
+ transient: true,
159
+ })
150
160
 
151
161
  if (agent._pendingReminders.length > 0) {
152
162
  for (const reminder of agent._pendingReminders) {
@@ -183,7 +193,13 @@ export async function prepareRun(agent, input, callbacks, {
183
193
  } : subagentTool
184
194
 
185
195
  const depthOnly = depth === 0 ? [filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, advisorTool]
186
- : agent._role === "eng-coder" ? [advisorTool, verifyTool] : []
196
+ // Write-permission coder sub-agents (subagent role="coder" + escalate surgeon): the
197
+ // system prompt names verify (system.md) and advisor (discipline.md) — without them a
198
+ // surgeon hit "unknown tool" and fell back to bash node --check / npm test to
199
+ // self-verify (2026-08-16 deepseek surgeon diagnosis; plugin parity).
200
+ : agent._role === "eng-coder" ? [advisorTool, verifyTool]
201
+ : agent._role === "coder" ? [verifyTool, advisorTool]
202
+ : []
187
203
  const tools = [...agent.tools, taskTool, planTool, timerTool, ...depthOnly]
188
204
  const toolSchemas = tools.map(toOpenAISchema)
189
205
  const toolByName = new Map(tools.map((t) => [t.name, t]))
@@ -240,7 +240,9 @@ export function normalizeToolPairing(messages) {
240
240
  rest.push(m)
241
241
  }
242
242
  }
243
- if (toolById.size === 0) return messages // no tool messages nothing to enforce
243
+ if (toolById.size === 0 && !messages.some((m) => m.role === "assistant" && m.tool_calls?.length)) {
244
+ return messages // no tool messages AND no tool_calls declared — nothing to enforce
245
+ }
244
246
  const out = []
245
247
  for (const m of rest) {
246
248
  out.push(m)
package/src/session.mjs CHANGED
@@ -325,8 +325,14 @@ export function saveSession(agent) {
325
325
  // _fullHistory is written at the source via pushReal — no flush needed here.
326
326
  // history = FULL, never-compacted (human-readable; VS Code panel & CLI resume read this)
327
327
  // contextHistory = machine context (possibly compacted) so CLI resume keeps the token savings
328
+ // Human line: transient machine injections never enter the readable record.
328
329
  const history = (agent._fullHistory ?? agent.history).filter((m) => !m.transient && !isLegacyTransient(m))
329
- const contextHistory = agent.history.filter((m) => !m.transient && !isLegacyTransient(m))
330
+ // Machine line (contextHistory): KEEP transient messages — resume must rebuild the
331
+ // machine line byte-identical to what the provider cache saw. Dropping them made every
332
+ // process restart diverge at the first injection position (git/OS/time reminders are
333
+ // re-injected with FRESH content) → whole-prefix cache miss on the CLI's very first
334
+ // request of each session (2026-08-16 cache-hit report; Kimi review).
335
+ const contextHistory = agent.history.filter((m) => !isLegacyTransient(m))
330
336
  const data = {
331
337
  version: 2,
332
338
  cwd: agent.cwd,
package/src/skills.mjs CHANGED
@@ -52,6 +52,10 @@ async function loadSkillsFromDir(dir) {
52
52
  let entries
53
53
  try {
54
54
  entries = await readdir(dir, { withFileTypes: true })
55
+ // Deterministic order: readdir order is filesystem-dependent; an unsorted scan would
56
+ // reshuffle the skills listing → system prompt byte change with zero content change
57
+ // (2026-08-16 cache audit — that silently misses the provider prefix cache).
58
+ entries.sort((a, b) => a.name.localeCompare(b.name))
55
59
  } catch {
56
60
  return []
57
61
  }